new-db-structure-convertion #1

Merged
Quildra merged 22 commits from new-db-structure-convertion into master 12 months ago
  1. 32
      .vscode/launch.json
  2. 1
      package.json
  3. 262
      src/app/app.component.ts
  4. 4
      src/app/app.routes.ts
  5. 50
      src/app/core/models/plan.model.ts
  6. 6
      src/app/core/models/pokemon.model.ts
  7. 77
      src/app/core/services/plan.service.ts
  8. 235
      src/app/core/services/pokemon.service.ts
  9. 25
      src/app/core/services/search.service.ts
  10. 25
      src/app/features/plan/plan-game/plan-game.component.ts
  11. 266
      src/app/features/plan/plan-pokemon-details/plan-pokemon-details.component.ts
  12. 206
      src/app/features/plan/plan-pokemon/plan-pokemon.component.ts
  13. 265
      src/app/features/plan/plan-pokemon/plan-pokemonV2.component.ts
  14. 43
      src/app/features/plan/plan.component.ts
  15. 274
      src/app/features/plan/planV2.component.ts
  16. 108
      src/app/features/pokemon/pokemon-carousel/pokemon-carousel.component.ts
  17. 38
      src/app/features/pokemon/pokemon-cell/pokemon-cell.component.ts
  18. 97
      src/app/features/pokemon/pokemon-details/pokemon-details.component.ts
  19. 10
      src/app/features/pokemon/pokemon-grid/pokemon-grid.component.ts
  20. 38
      src/app/shared/directives/lazy-component.directive.ts
  21. 1
      src/assets/images/Male_and_female_sign.svg
  22. 1
      src/assets/images/Male_symbol_(fixed_width).svg
  23. 1
      src/assets/images/Venus_symbol_(fixed_width).svg
  24. BIN
      src/assets/images/games/_AlphaSapphire.png
  25. BIN
      src/assets/images/games/_Black.png
  26. BIN
      src/assets/images/games/_Black2.png
  27. BIN
      src/assets/images/games/_Blue.png
  28. BIN
      src/assets/images/games/_BrilliantDiamond.png
  29. BIN
      src/assets/images/games/_Crystal.png
  30. BIN
      src/assets/images/games/_Diamond.png
  31. BIN
      src/assets/images/games/_Emerald.png
  32. BIN
      src/assets/images/games/_FireRed.png
  33. BIN
      src/assets/images/games/_Gold.png
  34. BIN
      src/assets/images/games/_HeartGold.png
  35. BIN
      src/assets/images/games/_LeafGreen.png
  36. BIN
      src/assets/images/games/_LegendsArceus.png
  37. BIN
      src/assets/images/games/_LetsGoEevee.png
  38. BIN
      src/assets/images/games/_LetsGoPikachu.png
  39. BIN
      src/assets/images/games/_Moon.png
  40. BIN
      src/assets/images/games/_OmegaRuby.png
  41. BIN
      src/assets/images/games/_Pearl.png
  42. BIN
      src/assets/images/games/_Platinum.png
  43. BIN
      src/assets/images/games/_Red.png
  44. BIN
      src/assets/images/games/_Ruby.png
  45. BIN
      src/assets/images/games/_Sapphire.png
  46. BIN
      src/assets/images/games/_Scarlet.png
  47. BIN
      src/assets/images/games/_Shield.png
  48. BIN
      src/assets/images/games/_ShiningPearl.png
  49. BIN
      src/assets/images/games/_Silver.png
  50. BIN
      src/assets/images/games/_SoulSilver.png
  51. BIN
      src/assets/images/games/_Sun.png
  52. BIN
      src/assets/images/games/_Sword.png
  53. BIN
      src/assets/images/games/_UltraMoon.png
  54. BIN
      src/assets/images/games/_UltraSun.png
  55. BIN
      src/assets/images/games/_Violet.png
  56. BIN
      src/assets/images/games/_White.png
  57. BIN
      src/assets/images/games/_White2.png
  58. BIN
      src/assets/images/games/_X.png
  59. BIN
      src/assets/images/games/_Y.png
  60. BIN
      src/assets/images/games/_Yellow.png
  61. BIN
      src/assets/images/map.png
  62. BIN
      src/assets/images/map_selected.png
  63. BIN
      src/assets/images/noun-map-2527063.png
  64. 44
      src/assets/images/noun-map-2527063.svg
  65. BIN
      src/assets/images/noun-poke-square-3548794.png
  66. 40
      src/assets/images/noun-poke-square-3548794.svg
  67. BIN
      src/assets/images/poke_box.png
  68. 21
      src/assets/images/poke_box.svg
  69. BIN
      src/assets/images/poke_box_selected.png

32
.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,
}
]
}

1
package.json

@ -5,6 +5,7 @@
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
"build": "ng build", "build": "ng build",
"build_prod": "ng build --configuration production",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"test": "ng test", "test": "ng test",
"serve:ssr:origin-dex": "node dist/origin-dex/server/server.mjs" "serve:ssr:origin-dex": "node dist/origin-dex/server/server.mjs"

262
src/app/app.component.ts

@ -1,13 +1,23 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { CommonModule } from '@angular/common'; 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 { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatTabsModule } from '@angular/material/tabs'; 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 { AuthService } from './core/services/auth.service';
import { MatSidenavModule } from '@angular/material/sidenav'; import { MatSidenavModule } from '@angular/material/sidenav';
import { MatListModule } from '@angular/material/list'; import { MatListModule } from '@angular/material/list';
import { PokemonService } from './core/services/pokemon.service'; 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({ @Component({
selector: 'app-root', selector: 'app-root',
@ -21,43 +31,127 @@ import { PokemonService } from './core/services/pokemon.service';
MatButtonModule, MatButtonModule,
MatTabsModule, MatTabsModule,
MatSidenavModule, MatSidenavModule,
MatListModule MatListModule,
MatFormFieldModule,
MatInputModule,
MatAutocompleteModule,
ReactiveFormsModule
], ],
template: ` template: `
<mat-toolbar color="primary"> <mat-toolbar color="primary" class="top-bar">
<span>OriginDex</span> <div class="toolbar-left">
<span class="spacer"></span> <span>OriginDex</span>
<ng-container *ngIf="auth.isAuthenticated$ | async; else loginButtons">
<div class="search-container">
<mat-form-field appearance="fill" class="search-field">
<mat-label>Search Pokémon</mat-label>
<input type="text"
matInput
[formControl]="searchControl"
[matAutocomplete]="auto">
<mat-autocomplete #auto="matAutocomplete"
(optionSelected)="onSearchSelect($event)"
[displayWith]="displayFn">
<mat-option *ngFor="let result of filteredOptions | async" [value]="result">
{{ result.pokemon }}
<span *ngIf="result.boxNumber !== undefined">
(Box {{ result.boxNumber + 1 }})
</span>
<span *ngIf="result.game_id !== undefined">
({{ result.game_id }})
</span>
</mat-option>
</mat-autocomplete>
</mat-form-field>
</div>
</div>
<div class="image-container">
<img
[src]="isRouteSelected('/storage-carousel') ? 'assets/images/poke_box_selected.png' : 'assets/images/poke_box.png'"
[routerLink]="['/storage-carousel']"
(mouseover)="hoveredRoute = '/storage-carousel'"
(mouseout)="hoveredRoute = ''"
class="top-bar-icon"
>
<img
[src]="isRouteSelected('/efficiency') ? 'assets/images/map_selected.png' : 'assets/images/map.png'"
[routerLink]="['/efficiency']"
(mouseover)="hoveredRoute = '/efficiency'"
(mouseout)="hoveredRoute = ''"
class="top-bar-icon"
>
</div>
<div class="toolbar-right" *ngIf="auth.isAuthenticated$ | async; else loginButtons">
<span>Welcome, {{ auth.currentUser?.username }}!</span> <span>Welcome, {{ auth.currentUser?.username }}!</span>
<button mat-button (click)="auth.logout()">Logout</button> <button mat-button (click)="auth.logout()">Logout</button>
</ng-container> </div>
<ng-template #loginButtons> <ng-template #loginButtons>
<button mat-button routerLink="/auth/login">Login</button> <div class="toolbar-right">
<button mat-button routerLink="/auth/register">Register</button> <button mat-button routerLink="/auth/login">Login</button>
<button mat-button routerLink="/auth/register">Register</button>
</div>
</ng-template> </ng-template>
</mat-toolbar> </mat-toolbar>
<mat-sidenav-container class="content-container">
<mat-sidenav mode="side" opened> <div class="content">
<mat-nav-list> <router-outlet></router-outlet>
<a mat-list-item routerLink="/storage-carousel" routerLinkActive="active"> </div>
Storage Carousel
</a>
<a mat-list-item routerLink="/efficiency" routerLinkActive="active">
Efficiency Plan
</a>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content>
<div class="content">
<router-outlet></router-outlet>
</div>
</mat-sidenav-content>
</mat-sidenav-container>
`, `,
styles: [` styles: [`
.spacer { .top-bar {
flex: 1 1 auto; 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 { mat-toolbar {
@ -75,7 +169,7 @@ import { PokemonService } from './core/services/pokemon.service';
} }
.content { .content {
height: 100%; //height: 100%;
overflow: auto; overflow: auto;
} }
@ -94,10 +188,116 @@ import { PokemonService } from './core/services/pokemon.service';
`] `]
}) })
export class AppComponent { export class AppComponent {
hoveredRoute: string = '';
searchControl = new FormControl('');
filteredOptions: Observable<PokemonSearchResult[]>;
pokemonGroups: (Pokemon | null)[][] = [];
gamePlans: GamePlan[] = [];
constructor( constructor(
public auth: AuthService, 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<string, PokemonSearchResult>();
if(this.isRouteSelected("/storage-carousel")){
this.pokemonGroups.forEach((group, boxIndex) => {
group.forEach(pokemon => {
if (pokemon && pokemon.Name.toLowerCase().includes(searchTerm)) {
const uniqueKey = `${pokemon.Name}-${boxIndex}`;
results_map.set(uniqueKey, {
pokemon: pokemon.Name,
boxNumber: boxIndex,
});
}
});
});
}
else if (this.isRouteSelected("/efficiency")) {
this.gamePlans.forEach((gamePlan, planIndex) => {
for (let family in gamePlan.pokemon) {
if(!gamePlan.pokemon[family].evolve_to_augmented) {
continue
}
for (let pkmn of gamePlan.pokemon[family].evolve_to_augmented) {
if (pkmn.name.toLowerCase().includes(searchTerm)) {
const uniqueKey = `${pkmn.name}-${gamePlan.game_name}`;
results_map.set(uniqueKey, {
pokemon: pkmn.name,
game_id: gamePlan.game_name,
});
}
}
if(!gamePlan.pokemon[family].breed_for_augmented) {
continue
}
for (let pkmn of gamePlan.pokemon[family].breed_for_augmented) {
if (pkmn.name.toLowerCase().includes(searchTerm)) {
const uniqueKey = `${pkmn.name}-${gamePlan.game_name}`;
results_map.set(uniqueKey, {
pokemon: pkmn.name,
game_id: gamePlan.game_name,
});
}
}
}
});
}
const results: PokemonSearchResult[] = Array.from(results_map.values());
return results.slice(0, 10); // Limit to 10 results
}
displayFn(result: PokemonSearchResult): string {
return result?.pokemon || '';
}
onSearchSelect(event: any) {
const result: PokemonSearchResult = event.option.value;
this.searchService.setSelectedItem(result);
this.searchControl.setValue('');
} }
} }

4
src/app/app.routes.ts

@ -19,8 +19,8 @@ export const routes: Routes = [
}, },
{ {
path: 'efficiency', path: 'efficiency',
loadComponent: () => import('./features/plan/plan.component') loadComponent: () => import('./features/plan/planV2.component')
.then(m => m.PlanComponent), .then(m => m.PlanV2Component),
canActivate: [AuthGuard] canActivate: [AuthGuard]
}, },
{ {

50
src/app/core/models/plan.model.ts

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

6
src/app/core/models/pokemon.model.ts

@ -3,15 +3,17 @@ export interface Pokemon {
Name: string; Name: string;
Form: string | null; Form: string | null;
NationalDex: number; NationalDex: number;
Generation?: number; Generation: number;
StorableInHome?: boolean; StorableInHome?: boolean;
IsBabyForm?: boolean; IsBabyForm?: boolean;
Encounters?: PokemonEncounter[]; Encounters?: PokemonEncounter[];
MarkIcon?: string; MarkIcon?: string;
MarkName?: string; MarkName: string;
Image?: string; Image?: string;
IsDefault?: boolean; IsDefault?: boolean;
IsCaught?: boolean; IsCaught?: boolean;
IsGenderRelevant?: boolean;
EvolutionMethod?: string;
} }
export interface PokemonEncounter { export interface PokemonEncounter {

77
src/app/core/services/plan.service.ts

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

235
src/app/core/services/pokemon.service.ts

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; 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 { Pokemon, PokemonEncounter } from '../models/pokemon.model';
import { environment } from '../../../environments/environment.development'; import { environment } from '../../../environments/environment.development';
import { comparePfics } from '../utils/pfic-utils'; import { comparePfics } from '../utils/pfic-utils';
@ -12,41 +12,124 @@ export class PokemonService {
private apiUrl = environment.apiUrl; private apiUrl = environment.apiUrl;
private pokemonCache: Observable<(Pokemon | null)[][]> | null = null; private pokemonCache: Observable<(Pokemon | null)[][]> | null = null;
private pokemonGroups: (Pokemon | null)[][] = []; private pokemonGroups: (Pokemon | null)[][] = [];
private pokemonFormMap: Map<string, Pokemon> = new Map<string, Pokemon>();
private pokemonFormCache: Observable<(Pokemon)[]> | null = null;
constructor(private http: HttpClient) { } constructor(private http: HttpClient) { }
getPokemonList(): Observable<(Pokemon | null)[][]> { getPokemonBoxList(): Observable<(Pokemon | null)[][]> {
if (this.pokemonCache) { if (this.pokemonCache) {
return this.pokemonCache; return this.pokemonCache;
} }
this.pokemonCache = this.http.get<(any | null)[][]>(`${this.apiUrl}/pokemon`).pipe( this.pokemonCache = this.getPokemonList().pipe(
map(groups => groups.map(group => map((pokemonList) => {
group.map(pokemon => pokemon ? { const boxes: (Pokemon | null)[][] = [];
PFIC: pokemon.PFIC, let currentBox: (Pokemon | null)[] = [];
Name: pokemon.name, let currentGeneration = 0;
Form: pokemon.form_name, let currentDexNumber = 0;
NationalDex: pokemon.national_dex, let formsGroup: Pokemon[] = [];
Generation: pokemon.generation,
StorableInHome: pokemon.storable_in_home, for (const pokemon of pokemonList) {
IsBabyForm: pokemon.is_baby_form, // Start a new NationalDex group if needed
Encounters: pokemon.encounters || [], if (pokemon.NationalDex !== currentDexNumber) {
MarkIcon: pokemon.icon_path, // If formsGroup has Pokémon, add them to the current box
MarkName: pokemon.mark_name, if (formsGroup.length > 0) {
Image: this.getPokemonImageUrl(pokemon.PFIC), for (const form of formsGroup) {
IsDefault: pokemon.is_default || false, currentBox.push(form);
IsCaught: this.caughtPokemon.has(pokemon.PFIC) if (currentBox.length === 30) {
} as Pokemon : null) boxes.push([...currentBox]);
)), currentBox = [];
tap(groups => { }
this.pokemonGroups = groups; // Store the groups for later updates }
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) shareReplay(1)
); );
return this.pokemonCache; return this.pokemonCache;
} }
getPokemonList(){
if(this.pokemonFormCache) {
return this.pokemonFormCache;
}
this.pokemonFormCache = this.http.get<any[]>(`${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<PokemonEncounter[]> { getPokemonDetails(pfic: string): Observable<PokemonEncounter[]> {
return this.http.get<PokemonEncounter[]>(`${this.apiUrl}/pokemon/${pfic}`); return this.http.get<PokemonEncounter[]>(`${this.apiUrl}/pokemon/${pfic}`);
} }
@ -65,13 +148,12 @@ export class PokemonService {
} }
private updatePokemonCaughtStatus(pfic: string, isCaught: boolean) { private updatePokemonCaughtStatus(pfic: string, isCaught: boolean) {
this.pokemonGroups.forEach(group => { if(this.pokemonFormMap.has(pfic)) {
group.forEach(pokemon => { const pkmn = this.pokemonFormMap.get(pfic)
if (pokemon && pokemon.PFIC === pfic) { if (pkmn) {
pokemon.IsCaught = isCaught; pkmn.IsCaught = isCaught;
} }
}); }
});
} }
getCaughtPokemon(): Observable<string[]> { getCaughtPokemon(): Observable<string[]> {
@ -84,6 +166,58 @@ export class PokemonService {
return this.caughtPokemon.has(pfic); return this.caughtPokemon.has(pfic);
} }
getPokemonFromPFIC(pfic: string): Observable<Pokemon | null> {
// 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<any>(`${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<Pokemon | null> => {
console.error(`Error fetching Pokémon with PFIC ${pfic}:`, error);
return new Observable((observer) => {
observer.next(null);
observer.complete();
});
})
);
}
updateCaughtStatus(pfic: string, caught: boolean) { updateCaughtStatus(pfic: string, caught: boolean) {
if (caught) { if (caught) {
this.caughtPokemon.add(pfic); this.caughtPokemon.add(pfic);
@ -92,8 +226,40 @@ export class PokemonService {
} }
} }
getPokemonImageUrl(pfic: string): string { getPokemonImageUrl(data: Pokemon | null): string {
return `/assets/images/pokemon/${pfic}.png`; 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 { getMarkImageUrl(markName: string): string {
@ -104,10 +270,7 @@ export class PokemonService {
this.getCaughtPokemon().subscribe( this.getCaughtPokemon().subscribe(
pfics => { pfics => {
pfics.forEach(pfic => this.updateCaughtStatus(pfic, true)); pfics.forEach(pfic => this.updateCaughtStatus(pfic, true));
// If pokemon are already loaded, update their status pfics.forEach(pfic => this.updatePokemonCaughtStatus(pfic, true));
if (this.pokemonGroups.length > 0) {
pfics.forEach(pfic => this.updatePokemonCaughtStatus(pfic, true));
}
} }
); );
} }

25
src/app/core/services/search.service.ts

@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { Pokemon } from '../models/pokemon.model';
@Injectable({
providedIn: 'root',
})
export class SearchService {
// This subject will hold the selected item value from the search bar
private selectedItemSubject = new BehaviorSubject<any>(null);
// Observable to subscribe to when the selected item changes
selectedItem$ = this.selectedItemSubject.asObservable();
// Method to update the selected item
setSelectedItem(item: any) {
this.selectedItemSubject.next(item);
}
}
export interface PokemonSearchResult {
pokemon: string;
boxNumber?: number;
game_id?: string;
}

25
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" [alt]="game.game_name"
class="game-image" class="game-image"
> >
<mat-card-content>
<h3>{{ game.game_name }}</h3>
<p class="catch-count">
Pokémon to catch: {{ getTotalCatchCount() }}
</p>
</mat-card-content>
</mat-card> </mat-card>
`, `,
styles: [` styles: [`
@ -47,7 +41,6 @@ import { GamePlan } from '../../../core/models/plan.model';
.game-image { .game-image {
width: 100%; width: 100%;
height: 160px;
object-fit: cover; object-fit: cover;
} }
@ -73,12 +66,24 @@ export class PlanGameComponent {
@Output() gameSelect = new EventEmitter<GamePlan>(); @Output() gameSelect = new EventEmitter<GamePlan>();
getTotalCatchCount(): number { 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 { getGameBoxArt(): string {
// You'll need to implement this to return the correct box art URL switch(this.game.game_name){
return `/assets/images/games/${this.game.game_name.toLowerCase().replace(' ', '-')}.png`; case "Legends: Arceus": {
return `/assets/images/games/_LegendsArceus.png`;
}
default: {
return `/assets/images/games/_${this.game.game_name.replace(' ', '')}.png`;
}
}
} }
onSelect() { onSelect() {

266
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: `
<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);
}
);
}
}
}

206
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 { CommonModule } from '@angular/common';
import { MatExpansionModule } from '@angular/material/expansion'; import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatChipsModule } from '@angular/material/chips'; import { MatChipsModule } from '@angular/material/chips';
import { MatTooltipModule } from '@angular/material/tooltip'; 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 { LazyImgDirective } from '../../../shared/directives/lazy-img.directive';
import { PokemonService } from '../../../core/services/pokemon.service'; import { PokemonService } from '../../../core/services/pokemon.service';
import { Pokemon } from '../../../core/models/pokemon.model';
// Define an interface for the status update event // Define an interface for the status update event
interface PokemonStatusEvent { interface PokemonStatusEvent {
@ -35,58 +36,65 @@ interface PokemonStatusEvent {
<div class="pokemon-header"> <div class="pokemon-header">
<img <img
lazyImg lazyImg
[src]="pokemonService.getPokemonImageUrl(pokemon.pfic)" [src]="pokemonService.getPokemonImageUrl(this.representative_pokemon)"
[alt]="pokemon.name" [alt]="this.representative_pokemon?.Name"
class="pokemon-thumbnail" class="pokemon-thumbnail"
[class.grayscale]="pokemon.catch_count === 0" [class.grayscale]="pokemon_family.catch_count === 0"
> >
<div class="pokemon-info"> <div class="pokemon-info">
<div class="pokemon-name"> <div class="pokemon-name">
{{ pokemon.name }} {{ this.representative_pokemon?.Name }}
<span *ngIf="pokemon.form_name" class="form-name"> <span class="form-name">
({{ pokemon.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> </span>
</div> </div>
<div class="catch-info"> <div class="catch-info">
<img <img
src="/assets/images/pokeball_color.png" src="/assets/images/pokeball_color.png"
[class.grayscale]="pokemon.catch_count === 0" [class.grayscale]="pokemon_family.catch_count === 0"
class="pokeball-icon" class="pokeball-icon"
[matTooltip]="'Need: ' + pokemon.catch_count" [matTooltip]="'Need: ' + pokemon_family.catch_count"
> >
<span class="catch-count">{{ pokemon.catch_count }}</span> <span class="catch-count">{{ pokemon_family.catch_count }}</span>
</div> </div>
</div> </div>
</div> </div>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div class="targets-grid" *ngIf="hasTargets"> <div class="targets-grid" *ngIf="hasTargets">
<ng-container *ngIf="pokemon.evolve_to.length > 0"> <ng-container *ngIf="evolve_to.length > 0">
<div class="target-section"> <div class="target-section">
<h4>Evolution Targets</h4> <h4>Evolution Targets</h4>
<div class="target-cards"> <div class="target-cards">
<div <div
*ngFor="let target of pokemon.evolve_to" *ngFor="let target of evolve_to; trackBy: trackByPfic"
class="target-card" class="target-card"
[class.completed]="isTargetCompleted(target.pfic)" [class.completed]="isTargetCompleted(target.PFIC)"
> >
<img <img
lazyImg lazyImg
[src]="pokemonService.getPokemonImageUrl(target.pfic)" [src]="pokemonService.getPokemonImageUrl(target)"
[alt]="target.name" [alt]="target.Name"
class="target-image" class="target-image"
[class.grayscale]="isTargetCompleted(target.pfic)" [class.grayscale]="isTargetCompleted(target.PFIC)"
> >
<div class="target-details"> <div class="target-details">
<div class="target-name"> <div class="target-name">
{{ target.name }} {{ target.Name }}
<span *ngIf="target.form_name">({{ target.form_name }})</span> <span *ngIf="target.Form">({{ target.Form }})</span>
</div> </div>
<mat-chip-listbox> <mat-chip-listbox>
<mat-chip>{{ target.method }}</mat-chip> <mat-chip>Method _PLACEDHOLDER_</mat-chip>
<mat-chip color="primary" selected>Need: {{ target.count }}</mat-chip>
</mat-chip-listbox> </mat-chip-listbox>
</div> </div>
</div> </div>
@ -94,30 +102,29 @@ interface PokemonStatusEvent {
</div> </div>
</ng-container> </ng-container>
<ng-container *ngIf="pokemon.breed_for.length > 0"> <ng-container *ngIf="pokemon_family.breed_for.length > 0">
<div class="target-section"> <div class="target-section">
<h4>Breeding Targets</h4> <h4>Breeding Targets</h4>
<div class="target-cards"> <div class="target-cards">
<div <div
*ngFor="let target of pokemon.breed_for" *ngFor="let target of breed_for"
class="target-card" class="target-card"
[class.completed]="isTargetCompleted(target.pfic)" [class.completed]="isTargetCompleted(target.PFIC)"
> >
<img <img
lazyImg lazyImg
[src]="pokemonService.getPokemonImageUrl(target.pfic)" [src]="pokemonService.getPokemonImageUrl(target)"
[alt]="target.name" [alt]="target.Name"
class="target-image" class="target-image"
[class.grayscale]="isTargetCompleted(target.pfic)" [class.grayscale]="isTargetCompleted(target.PFIC)"
> >
<div class="target-details"> <div class="target-details">
<div class="target-name"> <div class="target-name">
{{ target.name }} {{ target.Name }}
<span *ngIf="target.form_name">({{ target.form_name }})</span> <span *ngIf="target.Form">({{ target.Form }})</span>
</div> </div>
<mat-chip-listbox> <mat-chip-listbox>
<mat-chip>Breed</mat-chip> <mat-chip>Breed</mat-chip>
<mat-chip color="primary" selected>Need: {{ target.count }}</mat-chip>
</mat-chip-listbox> </mat-chip-listbox>
</div> </div>
</div> </div>
@ -252,13 +259,97 @@ interface PokemonStatusEvent {
`] `]
}) })
export class PlanPokemonComponent { export class PlanPokemonComponent {
@Input() pokemon!: PlanPokemon; @Input() pokemon_family!: PokemonFamilyEntry;
@Output() statusUpdate = new EventEmitter<PokemonStatusEvent>(); @Output() statusUpdate = new EventEmitter<PokemonStatusEvent>();
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 { 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 { isTargetCompleted(pfic: string): boolean {
@ -271,14 +362,14 @@ export class PlanPokemonComponent {
let evolveCount = 0; let evolveCount = 0;
// Calculate breeding needs // 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 breedCount = 1; // We only need one for breeding, regardless of how many we breed
} }
// Calculate evolution needs // Calculate evolution needs
this.pokemon.evolve_to.forEach(target => { this.pokemon_family.evolve_to.forEach(target => {
if (!this.isTargetCompleted(target.pfic)) { if (!this.isTargetCompleted(target)) {
evolveCount += target.count; evolveCount += 1;
} }
}); });
@ -287,23 +378,54 @@ export class PlanPokemonComponent {
updateCatchCount() { updateCatchCount() {
const newCount = this.calculateTotalNeeded(); const newCount = this.calculateTotalNeeded();
if (newCount !== this.pokemon.catch_count) { if (newCount !== this.pokemon_family.catch_count) {
this.pokemon.catch_count = newCount; this.pokemon_family.catch_count = newCount;
if (newCount === 0) { if (newCount === 0) {
// Emit event to move to completed section // Emit event to move to completed section
this.statusUpdate.emit({ this.statusUpdate.emit({
pfic: this.pokemon.pfic, pfic: this.pokemon_family.representative,
caught: true, caught: true,
completed: 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 // Emit event to move back to active section
this.statusUpdate.emit({ this.statusUpdate.emit({
pfic: this.pokemon.pfic, pfic: this.pokemon_family.representative,
caught: false, caught: false,
completed: 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;
});
}
} }

265
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: `
<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);
}
}

43
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 { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { FormsModule } from '@angular/forms'; 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 { PlanService } from '../../core/services/plan.service';
import { GamePlan } from '../../core/models/plan.model'; import { GamePlan } from '../../core/models/plan.model';
import { PlanPokemonComponent } from "./plan-pokemon/plan-pokemon.component"; import { PlanPokemonComponent } from "./plan-pokemon/plan-pokemon.component";
import { ScrollingModule, CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
@Component({ @Component({
selector: 'app-plan', selector: 'app-plan',
@ -15,7 +16,8 @@ import { PlanPokemonComponent } from "./plan-pokemon/plan-pokemon.component";
MatCardModule, MatCardModule,
FormsModule, FormsModule,
PlanGameComponent, PlanGameComponent,
PlanPokemonComponent PlanPokemonComponent,
ScrollingModule
], ],
template: ` template: `
<div class="plan-container"> <div class="plan-container">
@ -34,13 +36,14 @@ import { PlanPokemonComponent } from "./plan-pokemon/plan-pokemon.component";
<div class="pokemon-section" *ngIf="selectedGame"> <div class="pokemon-section" *ngIf="selectedGame">
<h2>{{ selectedGame.game_name }} - Pokémon to Catch</h2> <h2>{{ selectedGame.game_name }} - Pokémon to Catch</h2>
<div class="pokemon-list"> <cdk-virtual-scroll-viewport [itemSize]="56" class="pokemon-viewport">
<app-plan-pokemon <div *cdkVirtualFor="let pokemon of selectedGame.pokemon | keyvalue; trackBy: trackByPfic">
*ngFor="let pokemon of selectedGame.pokemon" <app-plan-pokemon
[pokemon]="pokemon" [pokemon_family]="pokemon.value"
(statusUpdate)="onPokemonStatusUpdate($event)" (statusUpdate)="onPokemonStatusUpdate($event)"
></app-plan-pokemon> ></app-plan-pokemon>
</div> </div>
</cdk-virtual-scroll-viewport>
</div> </div>
</div> </div>
`, `,
@ -90,13 +93,23 @@ import { PlanPokemonComponent } from "./plan-pokemon/plan-pokemon.component";
border-radius: 8px; border-radius: 8px;
margin-bottom: 20px; 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 { export class PlanComponent implements OnInit {
@ViewChild(CdkVirtualScrollViewport) viewport!: CdkVirtualScrollViewport;
gamePlans: GamePlan[] = []; gamePlans: GamePlan[] = [];
selectedGame: GamePlan | null = null; selectedGame: GamePlan | null = null;
constructor(private planService: PlanService) {} constructor(private planService: PlanService, private cdr: ChangeDetectorRef) {}
ngOnInit() { ngOnInit() {
this.loadPlan(); this.loadPlan();
@ -114,10 +127,20 @@ export class PlanComponent implements OnInit {
} }
selectGame(game: GamePlan) { 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.selectedGame = game;
this.cdr.detectChanges();
} }
onPokemonStatusUpdate(event: { pfic: string, caught: boolean }) { onPokemonStatusUpdate(event: { pfic: string, caught: boolean }) {
this.loadPlan(); this.loadPlan();
} }
trackByPfic(index: number, item: any): string {
return item.key; // Assuming PFIC or another unique identifier is available
}
} }

274
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: `
<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');
}
}
}

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

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

@ -22,12 +22,12 @@ import { PokemonService } from '../../../core/services/pokemon.service';
<div class="pokemon-name">{{ pokemon.Name }}</div> <div class="pokemon-name">{{ pokemon.Name }}</div>
<img <img
lazyImg lazyImg
[src]="pokemonService.getPokemonImageUrl(pokemon.PFIC)" [src]="pokemonService.getPokemonImageUrl(pokemon)"
[alt]="pokemon.Name" [alt]="pokemon.Name"
class="pokemon-image" class="pokemon-image"
> >
<div class="pokemon-form"> <div class="pokemon-form">
{{ pokemon.Form !== 'Default' ? pokemon.Form : '-----' }} {{ getFormString(pokemon) }}
</div> </div>
<div class="pokemon-info"> <div class="pokemon-info">
<div class="pokeball-container"> <div class="pokeball-container">
@ -58,14 +58,14 @@ import { PokemonService } from '../../../core/services/pokemon.service';
background-color: #f9f9f9; background-color: #f9f9f9;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 10px; border-radius: 10px;
padding: 10px; padding: 5px;
text-align: center; text-align: center;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: space-between; //justify-content: space-between;
cursor: pointer; cursor: pointer;
height: 180px; height: 130px;
} }
.pokemon-cell.empty { .pokemon-cell.empty {
@ -76,25 +76,26 @@ import { PokemonService } from '../../../core/services/pokemon.service';
.pokemon-name { .pokemon-name {
font-weight: bold; font-weight: bold;
font-size: 0.8em; font-size: 0.8em;
margin-bottom: 5px; //margin-bottom: 5px;
height: 2.4em; /* Allows for 2 lines of text */ height: 1em; /* Allows for 2 lines of text */
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
} }
.pokemon-image { .pokemon-image {
width: 96px; width: 70px;
height: 96px; height: 70px;
object-fit: contain; object-fit: contain;
background-color: #f9f9f9; /* Light gray background for placeholder */ background-color: #f9f9f9; /* Light gray background for placeholder */
} }
.pokemon-form { .pokemon-form {
min-height: 2.4em; min-height: 2.4em;
line-height: 1.2em; //line-height: 1.2em;
font-style: italic; font-style: italic;
font-size: 0.7em; font-size: 0.7em;
margin-top: 5px; //margin-top: 5px;
margin-bottom: 2px;
} }
.pokemon-info { .pokemon-info {
display: flex; display: flex;
@ -169,4 +170,19 @@ export class PokemonCellComponent {
this.caught.emit(this.pokemon.PFIC); this.caught.emit(this.pokemon.PFIC);
} }
} }
getFormString(pokemon: Pokemon): string {
if (!pokemon.Form) {
return '-----'
}
var form = pokemon.Form
if(pokemon.IsGenderRelevant == false) {
form = form.replace("Female", "").replace("female", "")
form = form.replace("Male", "").replace("male", "")
}
if (form == "" || form == 'Default') {
return '-----'
}
return form
}
} }

97
src/app/features/pokemon/pokemon-details/pokemon-details.component.ts

@ -27,40 +27,51 @@ import { Observable } from 'rxjs';
<div class="details-content"> <div class="details-content">
<img <img
[src]="'/assets/images/pokemon/' + pokemon?.Image" [src]="pokemon?.Image"
[alt]="pokemon?.Name" [alt]="pokemon?.Name"
class="pokemon-detail-image" class="pokemon-detail-image"
> >
<div class="encounters-section"> <div class="scrollable-content">
<h3>Encounters</h3> <div class="encounters-section">
<ng-container *ngIf="encounters$ | async as encounters"> <h3>Encounters</h3>
<div *ngFor="let gameGroup of groupEncountersByGame(encounters)" class="game-encounters"> <ng-container *ngIf="encounters$ | async as encounters">
<button <div *ngFor="let gameGroup of groupEncountersByGame(encounters)" class="game-encounters">
class="collapsible" <button
(click)="toggleCollapsible($event)" class="collapsible"
[class.active]="isCollapsibleActive" (click)="toggleCollapsible($event)"
> [class.active]="isCollapsibleActive"
{{ gameGroup.game }} >
<span class="toggle-icon"></span> {{ gameGroup.game }}
</button> <span class="toggle-icon"></span>
<div class="encounter-list"> </button>
<div *ngFor="let encounter of gameGroup.encounters" class="encounter-item"> <div class="encounter-list">
<p> <div *ngFor="let encounter of gameGroup.encounters" class="encounter-item">
{{ encounter.location }} <p>
<ng-container *ngIf="encounter.day || encounter.time"> {{ encounter.location }}
({{ encounter.day || '' }} {{ encounter.time || '' }}) <ng-container *ngIf="encounter.day || encounter.time">
</ng-container> ({{ encounter.day || '' }} {{ encounter.time || '' }})
</p> </ng-container>
<div class="encounter-tags"> </p>
<span *ngIf="encounter.static_encounter" class="tag">Static</span> <div class="encounter-tags">
<span *ngIf="encounter.fishing" class="tag">Fishing</span> <span *ngIf="encounter.static_encounter" class="tag">Static</span>
<span *ngIf="encounter.starter" class="tag">Starter</span> <span *ngIf="encounter.fishing" class="tag">Fishing</span>
<span *ngIf="encounter.starter" class="tag">Starter</span>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </ng-container>
</ng-container> </div>
</div>
<div class="external-links">
<a
[href]="'https://bulbapedia.bulbagarden.net/wiki/' + pokemon?.Name + '_(Pokémon)'"
target="_blank"
rel="noopener noreferrer"
>
View on Bulbapedia
</a>
</div> </div>
</div> </div>
</div> </div>
@ -76,7 +87,7 @@ import { Observable } from 'rxjs';
right: -340px; right: -340px;
bottom: 0; bottom: 0;
transition: right 0.3s ease-in-out; transition: right 0.3s ease-in-out;
overflow-y: auto; overflow: hidden;
z-index: 1000; z-index: 1000;
} }
@ -102,6 +113,7 @@ import { Observable } from 'rxjs';
object-fit: contain; object-fit: contain;
margin: 0 auto 20px; margin: 0 auto 20px;
display: block; display: block;
flex-shrink: 0; /* Prevent image from shrinking */
} }
.game-encounters { .game-encounters {
@ -163,6 +175,35 @@ import { Observable } from 'rxjs';
font-size: 0.8em; font-size: 0.8em;
color: #666; color: #666;
} }
.details-content {
display: flex;
flex-direction: column;
height: 100%;
}
.scrollable-content {
flex: 1;
overflow-y: auto;
}
.external-links {
position: sticky;
bottom: 0; /* Ensures it's always at the bottom of the panel */
padding: 15px;
border-top: 1px solid #eee;
background: white;
z-index: 10; /* Keeps it above the scrollable content */
a {
color: #0645ad;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
`] `]
}) })
export class PokemonDetailsComponent { export class PokemonDetailsComponent {

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

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

38
src/app/shared/directives/lazy-component.directive.ts

@ -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();
}
}
}

1
src/assets/images/Male_and_female_sign.svg

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path style="fill:none;stroke:#000;stroke-width:.60000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" transform="translate(-.877 -.75)" d="M6 11.75V8.5m-1.25 1.75h2.5m.517-6.017L10.25 1.75m-1.77 0h1.77v1.77M8.5 6c0-.69-.28-1.314-.733-1.767A2.502 2.502 0 0 0 3.5 6c0 1.379 1.121 2.5 2.5 2.5S8.5 7.379 8.5 6Z"/></svg>

After

Width:  |  Height:  |  Size: 458 B

1
src/assets/images/Male_symbol_(fixed_width).svg

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path style="fill:none;stroke:#000;stroke-width:.6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" d="M7.28 7.22c0-.69-.28-1.315-.732-1.768A2.492 2.492 0 0 0 4.78 4.72a2.502 2.502 0 0 0-2.5 2.5 2.5 2.5 0 0 0 5 0zm.67-4.94h1.77v1.77M6.547 5.452l3.171-3.171"/></svg>

After

Width:  |  Height:  |  Size: 403 B

1
src/assets/images/Venus_symbol_(fixed_width).svg

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path style="fill:none;stroke:#000;stroke-width:.6;stroke-miterlimit:4;stroke-dasharray:none;stroke-linejoin:round;stroke-linecap:round" d="M6 11V7M4 9h4m1-5a3 3 0 0 1-3 3 3 3 0 0 1-3-3 3 3 0 0 1 3-3 3 3 0 0 1 3 3Z"/></svg>

After

Width:  |  Height:  |  Size: 306 B

BIN
src/assets/images/games/_AlphaSapphire.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
src/assets/images/games/_Black.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
src/assets/images/games/_Black2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
src/assets/images/games/_Blue.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
src/assets/images/games/_BrilliantDiamond.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
src/assets/images/games/_Crystal.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
src/assets/images/games/_Diamond.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
src/assets/images/games/_Emerald.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/assets/images/games/_FireRed.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/assets/images/games/_Gold.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
src/assets/images/games/_HeartGold.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
src/assets/images/games/_LeafGreen.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
src/assets/images/games/_LegendsArceus.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
src/assets/images/games/_LetsGoEevee.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
src/assets/images/games/_LetsGoPikachu.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
src/assets/images/games/_Moon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
src/assets/images/games/_OmegaRuby.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
src/assets/images/games/_Pearl.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
src/assets/images/games/_Platinum.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
src/assets/images/games/_Red.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
src/assets/images/games/_Ruby.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
src/assets/images/games/_Sapphire.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
src/assets/images/games/_Scarlet.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
src/assets/images/games/_Shield.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/assets/images/games/_ShiningPearl.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
src/assets/images/games/_Silver.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
src/assets/images/games/_SoulSilver.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/assets/images/games/_Sun.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
src/assets/images/games/_Sword.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
src/assets/images/games/_UltraMoon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
src/assets/images/games/_UltraSun.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
src/assets/images/games/_Violet.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
src/assets/images/games/_White.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/assets/images/games/_White2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/assets/images/games/_X.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
src/assets/images/games/_Y.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
src/assets/images/games/_Yellow.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/assets/images/map.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src/assets/images/map_selected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
src/assets/images/noun-map-2527063.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

44
src/assets/images/noun-map-2527063.svg

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
data-name="LINE BLACK"
viewBox="0 0 32 40"
x="0px"
y="0px"
version="1.1"
id="svg2"
sodipodi:docname="noun-map-2527063.svg"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="20.325"
inkscape:cx="16.01476"
inkscape:cy="8.1918819"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<title
id="title1">map, place, destination, direction, quest, rpg, game</title>
<g
data-name="dungeon maps copy 3"
id="g1">
<path
d="M9,14a1,1,0,0,1,1-1h2V11a1,1,0,0,1,2,0v2h7V9a1,1,0,0,1,2,0v4h1a1,1,0,0,1,0,2H18v6a1,1,0,0,1-2,0V19H14v4a1,1,0,0,1-2,0V19H11a1,1,0,0,1,0-2h5V15H10A1,1,0,0,1,9,14ZM30,6V28a1,1,0,0,1-1,1H6a4,4,0,0,1-4-4V7A4,4,0,0,1,6,3,1,1,0,0,1,7,4V5H29A1,1,0,0,1,30,6ZM4,7V21.55a3.85,3.85,0,0,1,1-.42V5.28A2,2,0,0,0,4,7ZM28,7H7V22a1,1,0,0,1-1,1,2,2,0,0,0,0,4H28ZM25,19a1,1,0,0,1,1,1v4a1,1,0,0,1-1,1H21a1,1,0,0,1-1-1V20a1,1,0,0,1,1-1Zm-1,2H22v2h2Z"
id="path1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
src/assets/images/noun-poke-square-3548794.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

40
src/assets/images/noun-poke-square-3548794.svg

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
data-name="Layer 1"
viewBox="0 0 100 125"
x="0px"
y="0px"
version="1.1"
id="svg2"
sodipodi:docname="noun-poke-square-3548794.svg"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="6.504"
inkscape:cx="50.046125"
inkscape:cy="66.343788"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<title
id="title1">Artboard 27</title>
<path
d="M70,19H30A11,11,0,0,0,19,30V70A11,11,0,0,0,30,81H70A11,11,0,0,0,81,70V30A11,11,0,0,0,70,19ZM30,25H70a5,5,0,0,1,5,5V47H62.64a13,13,0,0,0-25.27,0H25V30A5,5,0,0,1,30,25ZM57,50a7,7,0,1,1-7-7A7,7,0,0,1,57,50ZM70,75H30a5,5,0,0,1-5-5V53H37.36a13,13,0,0,0,25.27,0H75V70A5,5,0,0,1,70,75Z"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
src/assets/images/poke_box.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

21
src/assets/images/poke_box.svg

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
data-name="Layer 1"
viewBox="0 0 6200 7750"
x="0px"
y="0px"
version="1.1"
id="svg2"
width="6200"
height="7750"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<title
id="title1">Artboard 27</title>
<path
d="m 7896.4759,-3080.477 h -40 a 11,11 0 0 0 -11,11 v 40 a 11,11 0 0 0 11,11 h 40 a 11,11 0 0 0 11,-11 v -40 a 11,11 0 0 0 -11,-11 z m -40,6 h 40 a 5,5 0 0 1 5,5 v 17 h -12.36 a 13,13 0 0 0 -25.27,0 h -12.37 v -17 a 5,5 0 0 1 5,-5 z m 27,25 a 7,7 0 1 1 -7,-7 7,7 0 0 1 7,7 z m 13,25 h -40 a 5,5 0 0 1 -5,-5 v -17 h 12.36 a 13,13 0 0 0 25.27,0 h 12.37 v 17 a 5,5 0 0 1 -5,5 z"
id="path1"
style="fill:#ffffff;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 805 B

BIN
src/assets/images/poke_box_selected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Loading…
Cancel
Save