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 { |
export interface GamePlan { |
||||
game_name: string; |
game_name: string; |
||||
game_id: number; |
pokemon: Record<string, PokemonFamilyEntry>; |
||||
pokemon: PlanPokemon[]; |
} |
||||
} |
|
||||
|
export interface PokemonFamilyEntry { |
||||
export interface PlanPokemon { |
family_pfic?: string; |
||||
pfic: string; |
representative: string; |
||||
name: string; |
catch_count: number; |
||||
form_name?: string; |
caught_count: number; |
||||
catch_count: number; |
evolve_to: string[]; |
||||
evolve_to: EvolutionTarget[]; |
breed_for: string[]; |
||||
breed_for: BreedingTarget[]; |
Any?: number; |
||||
} |
Male?: number; |
||||
|
Female?: number; |
||||
export interface EvolutionTarget { |
evolve_to_augmented?: PokemonEntry[] |
||||
pfic: string; |
breed_for_augmented?: PokemonEntry[] |
||||
name: string; |
} |
||||
form_name?: string; |
|
||||
method: string; |
interface PokemonEntry { |
||||
count: number; |
pfic: string |
||||
} |
name: string |
||||
|
} |
||||
export interface BreedingTarget { |
|
||||
pfic: string; |
|
||||
name: string; |
|
||||
form_name?: string; |
|
||||
count: number; |
|
||||
} |
|
||||
@ -1,56 +1,69 @@ |
|||||
import { Injectable } from '@angular/core'; |
import { Injectable } from '@angular/core'; |
||||
import { HttpClient } from '@angular/common/http'; |
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 { environment } from '../../../environments/environment.development'; |
||||
import { GamePlan } from '../models/plan.model'; |
import { GamePlan, PokemonFamilyEntry } from '../models/plan.model'; |
||||
|
import { PokemonService } from './pokemon.service'; |
||||
|
|
||||
@Injectable({ |
@Injectable({ |
||||
providedIn: 'root' |
providedIn: 'root' |
||||
}) |
}) |
||||
export class PlanService { |
export class PlanService { |
||||
private caughtPokemon = new Set<string>(); |
|
||||
private gameUpdates = new Subject<{gameId: number, total: number}>(); |
private gameUpdates = new Subject<{gameId: number, total: number}>(); |
||||
|
|
||||
gameUpdates$ = this.gameUpdates.asObservable(); |
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[]> { |
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) { |
private calculateGameTotal(game: GamePlan): number { |
||||
if (caught) { |
var sum = 0; |
||||
this.caughtPokemon.add(pfic); |
for(const family in game.pokemon) |
||||
} else { |
{ |
||||
this.caughtPokemon.delete(pfic); |
sum += game.pokemon[family].catch_count; |
||||
} |
} |
||||
// Trigger recalculation of affected games
|
return sum |
||||
this.recalculateAffectedGames(pfic); |
|
||||
} |
} |
||||
|
|
||||
private recalculateAffectedGames(pfic: string) { |
updateCaughtCount(family: PokemonFamilyEntry) { |
||||
// This would need to check all games for the affected Pokemon
|
for(const plan of this.gamePlan) { |
||||
// and update their totals accordingly
|
if(family.family_pfic && family.family_pfic in plan.pokemon) { |
||||
this.getPlan().pipe(take(1)).subscribe(games => { |
let pokemon_family = plan.pokemon[family.family_pfic] |
||||
games.forEach(game => { |
let count = 0; |
||||
const affectedPokemon = game.pokemon.find(p => |
|
||||
p.pfic === pfic || |
for( const pfic of pokemon_family.evolve_to) { |
||||
p.evolve_to.some(e => e.pfic === pfic) || |
if (this.pokemonService.isTargetCompleted(pfic)) { |
||||
p.breed_for.some(b => b.pfic === pfic) |
count += 1; |
||||
); |
} |
||||
if (affectedPokemon) { |
|
||||
this.gameUpdates.next({ |
|
||||
gameId: game.game_id, |
|
||||
total: this.calculateGameTotal(game) |
|
||||
}); |
|
||||
} |
} |
||||
}); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private calculateGameTotal(game: GamePlan): number { |
for( const pfic of pokemon_family.breed_for) { |
||||
return game.pokemon.reduce((total, pokemon) => total + pokemon.catch_count, 0); |
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 |