diff --git a/PTG/src/App.tsx b/PTG/src/App.tsx
index 18749fd..4a616aa 100644
--- a/PTG/src/App.tsx
+++ b/PTG/src/App.tsx
@@ -1,13 +1,28 @@
+import { useEffect, useState } from "react";
import Header from "./Header.tsx"
import ChoosePokemon from "./ChoosePokemon.tsx";
+import PokemonGrid from "./PokemonGrid.tsx";
+import { loadStoredFilters, saveStoredFilters } from "./pokemonFilters.ts";
export default function App(){
-
+ const [selectedGame, setSelectedGame] = useState(() => loadStoredFilters().selectedGame);
+ const [selectedType, setSelectedType] = useState(() => loadStoredFilters().selectedType);
+
+ useEffect(() => {
+ saveStoredFilters({ selectedGame, selectedType });
+ }, [selectedGame, selectedType]);
+
return(
- );
-}
\ No newline at end of file
+ );
+}
diff --git a/PTG/src/ChoosePokemon.css b/PTG/src/ChoosePokemon.css
new file mode 100644
index 0000000..d3edf93
--- /dev/null
+++ b/PTG/src/ChoosePokemon.css
@@ -0,0 +1,5 @@
+.chooseType {
+ field-sizing: content;
+ width: fit-content;
+ max-width: 100%;
+}
diff --git a/PTG/src/ChoosePokemon.tsx b/PTG/src/ChoosePokemon.tsx
index 92e014b..d2e7c78 100644
--- a/PTG/src/ChoosePokemon.tsx
+++ b/PTG/src/ChoosePokemon.tsx
@@ -1,76 +1,72 @@
-import {useState} from "react"
-
-
-
-function ChoosePokemon(){
- const [selectedGen, setSelectedGen] = useState('');
- const [selectedType, setSelectedType] = useState('');
-
- type Gen = {
- label: string,
- value: string
- }
-
- let gens: Gen[] = [
- {label: 'Gen I', value: 'gen_i'},
- {label: 'Gen II', value: 'gen_ii'},
- {label: 'Gen III', value: 'gen_iii'},
- {label: 'Gen IV', value: 'gen_iv'},
- {label: 'Gen V', value: 'gen_v'},
- {label: 'Gen VI', value: 'gen_vi'},
- {label: 'Gen VII', value: 'gen_vii'},
- {label: 'Gen VIII', value: 'gen_viii'},
- {label: 'Gen IX', value: 'gen_ix'},
- ]
-
- let types: Gen[] = [
- {label: 'Any', value: 'any'},
- {label: 'Fire', value: 'fire'},
- {label: 'Water', value: 'water'},
- {label: 'Grass', value: 'grass'},
- {label: 'Electric', value: 'electric'},
- {label: 'Rock', value: 'rock'},
- {label: 'Ground', value: 'ground'},
- {label: 'Flying', value: 'flying'},
- {label: 'Dragon', value: 'dragon'},
- {label: 'Bug', value: 'bug'},
- {label: 'Poison', value: 'poison'},
- {label: 'Psychic', value: 'psychic'},
- {label: 'Normal', value: 'normal'},
- {label: 'Fighting', value: 'fighting'},
- {label: 'Ghost', value: 'ghost'},
- {label: 'Ice', value: 'ice'},
- {label: 'Steel', value: 'steel'},
- {label: 'Dark', value: 'dark'},
- {label: 'Fairy', value: 'fairy'}
- ]
-
- function changeTypeArray(){
- if (selectedGen === 'gen_i') return types.slice(0, 16);
- else if (selectedGen === 'gen_ii' || selectedGen === 'gen_iii' ||
- selectedGen === 'gen_iv' || selectedGen === 'gen_v'){
- return types.slice(0, 18);
- }
- return types;
- }
-
- function handleGenChange(e: any) { setSelectedGen(e.target.value); }
-
- function handleTypeChange(e: any) { setSelectedType(e.target.value); }
-
- let newTypes : Gen[] = changeTypeArray();
+import { useEffect, useState } from "react";
+import "./ChoosePokemon.css";
+import { fetchGames, type GameOption } from "./games";
+import { fetchTypes, type TypeOption } from "./pokemonTypes";
+
+type TypeChoice = {
+ label: string,
+ value: string
+}
+
+const ANY_TYPE: TypeChoice = { label: 'Any Type', value: 'any' };
+
+type ChoosePokemonProps = {
+ selectedGame: string;
+ selectedType: string;
+ onGameChange: (value: string) => void;
+ onTypeChange: (value: string) => void;
+}
+
+function ChoosePokemon({ selectedGame, selectedType, onGameChange, onTypeChange }: ChoosePokemonProps){
+ const [games, setGames] = useState([]);
+ const [gamesError, setGamesError] = useState(null);
+ const [types, setTypes] = useState([]);
+ const [typesError, setTypesError] = useState(null);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ fetchGames()
+ .then((data) => {
+ if (!cancelled) setGames(data);
+ })
+ .catch((err: Error) => {
+ if (!cancelled) setGamesError(err.message);
+ });
+
+ fetchTypes()
+ .then((data) => {
+ if (!cancelled) setTypes(data);
+ })
+ .catch((err: Error) => {
+ if (!cancelled) setTypesError(err.message);
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ function handleGameChange(e: React.ChangeEvent) { onGameChange(e.target.value); }
+
+ function handleTypeChange(e: React.ChangeEvent) { onTypeChange(e.target.value); }
+
+ const newTypes: TypeChoice[] = [ANY_TYPE, ...types];
+
+ if (gamesError) return Failed to load games: {gamesError}
;
+ if (typesError) return Failed to load types: {typesError}
;
return(
<>
@@ -86,18 +82,24 @@ function ChoosePokemon(){
))}
-
+
>
);
}
-export default function generateTeam(){
-
+// Filter controls plus the (not yet wired up) team generation button.
+export default function generateTeam({ selectedGame, selectedType, onGameChange, onTypeChange }: ChoosePokemonProps){
+
return(
<>
-
+
>
-
+
);
-}
\ No newline at end of file
+}
diff --git a/PTG/src/PokemonGrid.css b/PTG/src/PokemonGrid.css
new file mode 100644
index 0000000..8d25505
--- /dev/null
+++ b/PTG/src/PokemonGrid.css
@@ -0,0 +1,47 @@
+.pokemonGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+ gap: 1rem;
+ padding: 1rem;
+}
+
+.pokemonCard {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ padding: 0.5rem;
+ text-align: center;
+}
+
+.pokemonCardImage {
+ position: relative;
+ width: 100%;
+ max-width: 120px;
+}
+
+.pokemonCardImage > img {
+ width: 100%;
+ height: auto;
+ display: block;
+}
+
+.pokemonCardTypes {
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ display: flex;
+ gap: 2px;
+}
+
+.pokemonCardTypes img {
+ width: 20px;
+ height: 20px;
+ filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.6));
+}
+
+.pokemonGridError {
+ color: red;
+ text-align: center;
+}
diff --git a/PTG/src/PokemonGrid.tsx b/PTG/src/PokemonGrid.tsx
new file mode 100644
index 0000000..081b8e5
--- /dev/null
+++ b/PTG/src/PokemonGrid.tsx
@@ -0,0 +1,164 @@
+import { useEffect, useState } from "react";
+import "./PokemonGrid.css";
+import { fetchGameSpeciesIds } from "./games";
+import { fetchPokemonTypeMap, TYPE_ICON_URL, type PokemonType } from "./pokemonTypes";
+
+const POKEMON_COUNT = 1025;
+const LIST_URL = `https://pokeapi.co/api/v2/pokemon?limit=${POKEMON_COUNT}`;
+const ARTWORK_URL = (id: number) =>
+ `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${id}.png`;
+const TYPE_URL = (type: string) => `https://pokeapi.co/api/v2/type/${type}`;
+
+type Pokemon = {
+ id: number;
+ name: string;
+ image: string;
+};
+
+function capitalize(name: string) {
+ return name.charAt(0).toUpperCase() + name.slice(1);
+}
+
+type PokemonGridProps = {
+ selectedGame: string;
+ selectedType: string;
+};
+
+export default function PokemonGrid({ selectedGame, selectedType }: PokemonGridProps) {
+ const [pokemon, setPokemon] = useState([]);
+ const [error, setError] = useState(null);
+ const [typeNames, setTypeNames] = useState | null>(null);
+ const [typeError, setTypeError] = useState(null);
+ const [gameIds, setGameIds] = useState | null>(null);
+ const [gameError, setGameError] = useState(null);
+ const [typeMap, setTypeMap] = useState