Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,881 changes: 0 additions & 2,881 deletions PTG/package-lock.json

This file was deleted.

1 change: 1 addition & 0 deletions PTG/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@tanstack/react-query": "^5.103.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-router": "^8.3.1",
Expand Down
18 changes: 18 additions & 0 deletions PTG/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions PTG/pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
minimumReleaseAgeExclude:
- '@tanstack/query-core@5.103.0'
- '@tanstack/react-query@5.103.0'
12 changes: 12 additions & 0 deletions PTG/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { useEffect, useState } from "react";
import Header from "./Header.tsx"
import Footer from "./Footer.tsx"
import ChoosePokemon from "./ChoosePokemon.tsx";
import Team from "./Team.tsx";
import PokemonGrid from "./PokemonGrid.tsx";
import { loadStoredFilters, saveStoredFilters } from "./pokemonFilters.ts";
import { usePokemonTeam } from "./usePokemonTeam.ts";

export default function App(){
const [selectedGame, setSelectedGame] = useState(() => loadStoredFilters().selectedGame);
Expand All @@ -13,6 +15,8 @@ export default function App(){
saveStoredFilters({ selectedGame, selectedType });
}, [selectedGame, selectedType]);

const { team, pokemonById, typeMap, allFavourited, generateTeam, toggleFavourite } = usePokemonTeam(selectedGame, selectedType);

return(
<div>
<Header />
Expand All @@ -22,6 +26,14 @@ export default function App(){
onGameChange={setSelectedGame}
onTypeChange={setSelectedType}
/>
<Team
team={team}
pokemonById={pokemonById}
typeMap={typeMap}
allFavourited={allFavourited}
generateTeam={generateTeam}
toggleFavourite={toggleFavourite}
/>
<PokemonGrid selectedGame={selectedGame} selectedType={selectedType} />
<Footer/>
</div>
Expand Down
65 changes: 14 additions & 51 deletions PTG/src/ChoosePokemon.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import "./ChoosePokemon.css";
import { fetchGames, type GameOption } from "./games";
import { fetchTypes, type TypeOption } from "./pokemonTypes";
import { fetchGames } from "./games";
import { fetchTypes } from "./pokemonTypes";

type TypeChoice = {
label: string,
Expand All @@ -17,44 +17,24 @@ type ChoosePokemonProps = {
onTypeChange: (value: string) => void;
}

function ChoosePokemon({ selectedGame, selectedType, onGameChange, onTypeChange }: ChoosePokemonProps){
const [games, setGames] = useState<GameOption[]>([]);
const [gamesError, setGamesError] = useState<string | null>(null);
const [types, setTypes] = useState<TypeOption[]>([]);
const [typesError, setTypesError] = useState<string | null>(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;
};
}, []);
export default function ChoosePokemon({ selectedGame, selectedType, onGameChange, onTypeChange }: ChoosePokemonProps){
const { data: games = [], error: gamesError } = useQuery({
queryKey: ["games"],
queryFn: fetchGames,
});
const { data: types = [], error: typesError } = useQuery({
queryKey: ["types"],
queryFn: fetchTypes,
});

function handleGameChange(e: React.ChangeEvent<HTMLSelectElement>) { onGameChange(e.target.value); }

function handleTypeChange(e: React.ChangeEvent<HTMLSelectElement>) { onTypeChange(e.target.value); }

const newTypes: TypeChoice[] = [ANY_TYPE, ...types];

if (gamesError) return <p className="pokemonGridError">Failed to load games: {gamesError}</p>;
if (typesError) return <p className="pokemonGridError">Failed to load types: {typesError}</p>;
if (gamesError) return <p className="pokemonGridError">Failed to load games: {gamesError.message}</p>;
if (typesError) return <p className="pokemonGridError">Failed to load types: {typesError.message}</p>;

return(
<>
Expand Down Expand Up @@ -86,20 +66,3 @@ function ChoosePokemon({ selectedGame, selectedType, onGameChange, onTypeChange
</>
);
}

// Filter controls plus the (not yet wired up) team generation button.
export default function generateTeam({ selectedGame, selectedType, onGameChange, onTypeChange }: ChoosePokemonProps){

return(
<>
<ChoosePokemon
selectedGame={selectedGame}
selectedType={selectedType}
onGameChange={onGameChange}
onTypeChange={onTypeChange}
/>
<button>Generate Team</button>
</>

);
}
137 changes: 8 additions & 129 deletions PTG/src/PokemonGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
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;
};
import { TYPE_ICON_URL, type PokemonType } from "./pokemonTypes";
import { useFilteredPokemon } from "./useFilteredPokemon";

function capitalize(name: string) {
return name.charAt(0).toUpperCase() + name.slice(1);
Expand All @@ -25,131 +12,23 @@ type PokemonGridProps = {
};

export default function PokemonGrid({ selectedGame, selectedType }: PokemonGridProps) {
const [pokemon, setPokemon] = useState<Pokemon[]>([]);
const [error, setError] = useState<string | null>(null);
const [typeNames, setTypeNames] = useState<Set<string> | null>(null);
const [typeError, setTypeError] = useState<string | null>(null);
const [gameIds, setGameIds] = useState<Set<number> | null>(null);
const [gameError, setGameError] = useState<string | null>(null);
const [typeMap, setTypeMap] = useState<Map<string, PokemonType[]> | null>(null);

useEffect(() => {
let cancelled = false;

fetch(LIST_URL)
.then((res) => {
if (!res.ok) throw new Error(`PokeAPI request failed: ${res.status}`);
return res.json();
})
.then((data: { results: { name: string; url: string }[] }) => {
if (cancelled) return;
const list = data.results.map(({ name, url }) => {
const id = Number(url.split("/").filter(Boolean).pop());
return { id, name, image: ARTWORK_URL(id) };
});
setPokemon(list);
})
.catch((err: Error) => {
if (!cancelled) setError(err.message);
});

return () => {
cancelled = true;
};
}, []);

useEffect(() => {
let cancelled = false;

// Decorative type icons; a failure here shouldn't block the rest of the grid.
fetchPokemonTypeMap()
.then((map) => {
if (!cancelled) setTypeMap(map);
})
.catch((err: Error) => {
console.error("Failed to load Pokémon types:", err.message);
});

return () => {
cancelled = true;
};
}, []);

useEffect(() => {
let cancelled = false;

if (!selectedType || selectedType === "any") {
setTypeNames(null);
setTypeError(null);
return;
}

fetch(TYPE_URL(selectedType))
.then((res) => {
if (!res.ok) throw new Error(`PokeAPI request failed: ${res.status}`);
return res.json();
})
.then((data: { pokemon: { pokemon: { name: string } }[] }) => {
if (cancelled) return;
setTypeNames(new Set(data.pokemon.map(({ pokemon }) => pokemon.name)));
setTypeError(null);
})
.catch((err: Error) => {
if (!cancelled) setTypeError(err.message);
});

return () => {
cancelled = true;
};
}, [selectedType]);

useEffect(() => {
let cancelled = false;

if (!selectedGame) {
setGameIds(null);
setGameError(null);
return;
}

fetchGameSpeciesIds(selectedGame)
.then((ids) => {
if (!cancelled) {
setGameIds(ids);
setGameError(null);
}
})
.catch((err: Error) => {
if (!cancelled) setGameError(err.message);
});

return () => {
cancelled = true;
};
}, [selectedGame]);

if (error) return <p className="pokemonGridError">Failed to load Pokémon: {error}</p>;
if (typeError) return <p className="pokemonGridError">Failed to load type: {typeError}</p>;
if (gameError) return <p className="pokemonGridError">Failed to load game: {gameError}</p>;
const { pokemon, typeMap, pokemonError, typeError, gameError } = useFilteredPokemon(selectedGame, selectedType);

// Each filter is resolved to a matching-id/name set independently above; a Pokémon must satisfy all of them.
const filtered = pokemon.filter(({ id, name }) => {
if (gameIds && !gameIds.has(id)) return false;
if (typeNames && !typeNames.has(name)) return false;
return true;
});
if (pokemonError) return <p className="pokemonGridError">Failed to load Pokémon: {pokemonError.message}</p>;
if (typeError) return <p className="pokemonGridError">Failed to load type: {typeError.message}</p>;
if (gameError) return <p className="pokemonGridError">Failed to load game: {gameError.message}</p>;

return (
<div className="pokemonGrid">
{filtered.map(({ id, name, image }) => {
{pokemon.map(({ id, name, image }) => {
const types = typeMap?.get(name);
return (
<div className="pokemonCard" key={id}>
<div className="pokemonCardImage">
<img src={image} alt={name} loading="lazy" />
{types && types.length > 0 && (
<div className="pokemonCardTypes">
{types.map((type) => (
{types.map((type: PokemonType) => (
<img key={type.id} src={TYPE_ICON_URL(type.id)} alt={type.name} title={capitalize(type.name)} />
))}
</div>
Expand Down
Loading