new-db-structure-convertion #1
new-db-structure-convertion into master 12 months ago
@ -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, |
|||
} |
|||
] |
|||
} |
|||
@ -1,29 +1,23 @@ |
|||
export interface GamePlan { |
|||
game_name: string; |
|||
game_id: number; |
|||
pokemon: PlanPokemon[]; |
|||
pokemon: Record<string, PokemonFamilyEntry>; |
|||
} |
|||
|
|||
export interface PlanPokemon { |
|||
pfic: string; |
|||
name: string; |
|||
form_name?: string; |
|||
export interface PokemonFamilyEntry { |
|||
family_pfic?: string; |
|||
representative: string; |
|||
catch_count: number; |
|||
evolve_to: EvolutionTarget[]; |
|||
breed_for: BreedingTarget[]; |
|||
caught_count: number; |
|||
evolve_to: string[]; |
|||
breed_for: string[]; |
|||
Any?: number; |
|||
Male?: number; |
|||
Female?: number; |
|||
evolve_to_augmented?: PokemonEntry[] |
|||
breed_for_augmented?: PokemonEntry[] |
|||
} |
|||
|
|||
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; |
|||
interface PokemonEntry { |
|||
pfic: string |
|||
name: string |
|||
} |
|||
@ -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<string>(); |
|||
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<GamePlan[]> { |
|||
return this.http.get<GamePlan[]>(`${environment.apiUrl}/plan`); |
|||
if (this.gamePlanCache) { |
|||
return this.gamePlanCache; |
|||
} |
|||
this.gamePlanCache = this.http.get<GamePlan[]>(`${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; |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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: ` |
|||
<mat-tab-group> |
|||
<!-- Evolution Targets Tab --> |
|||
<mat-tab label="Evolution Targets" *ngIf="evolve_to.length > 0"> |
|||
<div class="scrollable-content"> |
|||
<div |
|||
*ngFor="let target of evolve_to" |
|||
class="target-card" |
|||
[class.completed]="isTargetCompleted(target.PFIC)" |
|||
> |
|||
<img |
|||
lazyImg |
|||
[src]="pokemonService.getPokemonImageUrl(target)" |
|||
[alt]="target.Name" |
|||
class="target-image" |
|||
[class.grayscale]="isTargetCompleted(target.PFIC)" |
|||
> |
|||
<div class="pokeball-container"> |
|||
<img |
|||
src="/assets/images/pokeball_color.png" |
|||
[class.grayscale]="!target.IsCaught" |
|||
class="pokeball-icon" |
|||
(click)="onPokeballClick($event, target)" |
|||
> |
|||
</div> |
|||
<div class="target-details"> |
|||
<span class="target-name"> |
|||
{{ target.Name }} |
|||
<span *ngIf="target.Form">({{ target.Form }})</span> |
|||
</span> |
|||
<span *ngIf="target.EvolutionMethod">{{target?.EvolutionMethod}}</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</mat-tab> |
|||
|
|||
<!-- Breeding Targets Tab --> |
|||
<mat-tab label="Breeding Targets" *ngIf="breed_for.length > 0"> |
|||
<div class="scrollable-content"> |
|||
<div |
|||
*ngFor="let target of breed_for" |
|||
class="target-card" |
|||
[class.completed]="isTargetCompleted(target.PFIC)" |
|||
> |
|||
<img |
|||
lazyImg |
|||
[src]="pokemonService.getPokemonImageUrl(target)" |
|||
[alt]="target.Name" |
|||
class="target-image" |
|||
[class.grayscale]="isTargetCompleted(target.PFIC)" |
|||
> |
|||
<div class="target-details"> |
|||
<span class="target-name"> |
|||
{{ target.Name }} |
|||
<span *ngIf="target.Form">({{ target.Form }})</span> |
|||
</span> |
|||
<mat-chip-listbox> |
|||
<mat-chip>Breed</mat-chip> |
|||
</mat-chip-listbox> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</mat-tab> |
|||
</mat-tab-group> |
|||
`,
|
|||
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<PokemonCaughtStatusUpdate>(); |
|||
|
|||
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); |
|||
} |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -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: ` |
|||
<mat-card |
|||
class="pokemon-row" |
|||
[class.selected]="isSelected" |
|||
(click)="onSelect()"> |
|||
<div class="pokemon-header"> |
|||
<img |
|||
lazyImg |
|||
[src]="pokemonService.getPokemonImageUrl(this.representative_pokemon)" |
|||
[alt]="this.representative_pokemon?.Name" |
|||
class="pokemon-thumbnail" |
|||
> |
|||
|
|||
<div class="pokemon-info"> |
|||
<div class="pokemon-name"> |
|||
{{ this.representative_pokemon?.Name }} |
|||
<span class="form-name"> |
|||
<span *ngIf="this.pokemon_family?.Male"> |
|||
<img src="assets/images/Male_symbol_(fixed_width).svg" >: {{ this.pokemon_family.Male }} |
|||
</span> |
|||
<span *ngIf="this.pokemon_family?.Female"> |
|||
<img src="assets/images/Venus_symbol_(fixed_width).svg" >: {{ this.pokemon_family.Female }} |
|||
</span> |
|||
<span *ngIf="this.pokemon_family?.Any"> |
|||
<img src="assets/images/Male_and_female_sign.svg" >: {{ this.pokemon_family.Any }} |
|||
</span> |
|||
</span> |
|||
</div> |
|||
|
|||
<div class="catch-info"> |
|||
<img |
|||
src="/assets/images/pokeball_color.png" |
|||
[class.grayscale]="this.pokemon_family.catch_count - this.pokemon_family.caught_count === 0" |
|||
class="pokeball-icon" |
|||
> |
|||
<span class="catch-count">{{ this.pokemon_family.catch_count - this.pokemon_family.caught_count }}</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="progress-bar-container"> |
|||
<div class="progress-bar" [style.width.%]="calculateCatchProgress()"></div> |
|||
</div> |
|||
</mat-card> |
|||
`,
|
|||
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<PokemonStatusEvent>(); |
|||
@Output() familySelected = new EventEmitter<PokemonFamilyEntry>(); |
|||
|
|||
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); |
|||
} |
|||
} |
|||
|
|||
@ -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: ` |
|||
<div class="plan-container"> |
|||
<div class="games-section"> |
|||
<div class="games-scroll"> |
|||
<div class="games-list"> |
|||
<app-plan-game |
|||
*ngFor="let game of gamePlans" |
|||
[game]="game" |
|||
[isSelected]="selectedGame?.game_name === game.game_name" |
|||
(gameSelect)="selectGame($event)" |
|||
#gameComponent |
|||
></app-plan-game> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="content-section"> |
|||
<div class="game-stats" *ngIf="selectedGame"> |
|||
<h2>{{ selectedGame.game_name }} - Game Stats</h2> |
|||
<!-- Add game stats here --> |
|||
</div> |
|||
|
|||
<div class="middle-section"> |
|||
<div class="pokemon-section" *ngIf="selectedGame"> |
|||
<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> |
|||
</div> |
|||
|
|||
<div class="details-section"> |
|||
<h2>{{ selectedPokemon?.representative }} - Details</h2> |
|||
<app-plan-pokemon-details |
|||
*ngIf="selectedPokemon" |
|||
[pokemon_family]="selectedPokemon" |
|||
></app-plan-pokemon-details> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
`,
|
|||
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<ElementRef>; |
|||
@ViewChildren('familyComponent', { read: ElementRef }) familyElements!: QueryList<ElementRef>; |
|||
|
|||
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'); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,38 @@ |
|||
import { Directive, ElementRef, EventEmitter, Output, AfterViewInit, OnDestroy, NgZone } from '@angular/core'; |
|||
|
|||
@Directive({ |
|||
selector: '[appLazyLoad]', |
|||
standalone: true |
|||
}) |
|||
export class LazyLoadDirective implements AfterViewInit, OnDestroy { |
|||
@Output() lazyLoad = new EventEmitter<void>(); |
|||
|
|||
private observer!: IntersectionObserver; |
|||
|
|||
constructor(private element: ElementRef, private ngZone: NgZone) {} |
|||
|
|||
ngAfterViewInit(): void { |
|||
// Run the intersection observer outside Angular's zone
|
|||
this.ngZone.runOutsideAngular(() => { |
|||
this.observer = new IntersectionObserver(entries => { |
|||
entries.forEach(entry => { |
|||
if (entry.isIntersecting) { |
|||
// Re-enter Angular's zone to trigger the lazy load event
|
|||
this.ngZone.run(() => { |
|||
this.lazyLoad.emit(); |
|||
this.observer.disconnect(); // Disconnect after loading
|
|||
}); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
this.observer.observe(this.element.nativeElement); |
|||
}); |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
if (this.observer) { |
|||
this.observer.disconnect(); |
|||
} |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 458 B |
|
After Width: | Height: | Size: 403 B |
|
After Width: | Height: | Size: 306 B |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 805 B |
|
After Width: | Height: | Size: 1.1 KiB |