Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
23 changes: 19 additions & 4 deletions PTG/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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 [selectedGen, setSelectedGen] = useState(() => loadStoredFilters().selectedGen);
const [selectedType, setSelectedType] = useState(() => loadStoredFilters().selectedType);

useEffect(() => {
saveStoredFilters({ selectedGen, selectedType });
}, [selectedGen, selectedType]);

return(
<div>
<Header />
<ChoosePokemon />
<ChoosePokemon
selectedGen={selectedGen}
selectedType={selectedType}
onGenChange={setSelectedGen}
onTypeChange={setSelectedType}
/>
<PokemonGrid selectedGen={selectedGen} selectedType={selectedType} />
</div>

);
}
);
}
144 changes: 81 additions & 63 deletions PTG/src/ChoosePokemon.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,77 @@
import {useState} from "react"
import { useEffect, useState } from "react";
import { fetchGenerations, type GenerationOption } from "./generations";
import { fetchTypes, type TypeOption } from "./pokemonTypes";

type Gen = {
label: string,
value: string
}

const ANY_TYPE: Gen = { label: 'Any Type', value: 'any' };

function ChoosePokemon(){
const [selectedGen, setSelectedGen] = useState('');
const [selectedType, setSelectedType] = useState('');

type Gen = {
label: string,
value: string
}
function changeTypeArray(selectedGen: string, gens: GenerationOption[], types: TypeOption[]) {
const genIndex = gens.findIndex((gen) => gen.value === selectedGen);
const available = genIndex === -1
? types
: types.filter((type) => {
const typeGenIndex = gens.findIndex((gen) => gen.value === type.generation);
return typeGenIndex === -1 || typeGenIndex <= genIndex;
});
return [ANY_TYPE, ...available];
}

type ChoosePokemonProps = {
selectedGen: string;
selectedType: string;
onGenChange: (value: string) => void;
onTypeChange: (value: string) => void;
}

function ChoosePokemon({ selectedGen, selectedType, onGenChange, onTypeChange }: ChoosePokemonProps){
const [gens, setGens] = useState<GenerationOption[]>([]);
const [gensError, setGensError] = useState<string | null>(null);
const [types, setTypes] = useState<TypeOption[]>([]);
const [typesError, setTypesError] = useState<string | null>(null);

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

fetchGenerations()
.then((data) => {
if (!cancelled) setGens(data);
})
.catch((err: Error) => {
if (!cancelled) setGensError(err.message);
});

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);
fetchTypes()
.then((data) => {
if (!cancelled) setTypes(data);
})
.catch((err: Error) => {
if (!cancelled) setTypesError(err.message);
});

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

function handleGenChange(e: React.ChangeEvent<HTMLSelectElement>) {
const newGen = e.target.value;
onGenChange(newGen);
const availableTypes = changeTypeArray(newGen, gens, types);
if (!availableTypes.some((t) => t.value === selectedType)) {
onTypeChange('any');
}
return types;
}

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

function handleTypeChange(e: any) { setSelectedType(e.target.value); }

let newTypes : Gen[] = changeTypeArray();
let newTypes : Gen[] = changeTypeArray(selectedGen, gens, types);

if (gensError) return <p className="pokemonGridError">Failed to load generations: {gensError}</p>;
if (typesError) return <p className="pokemonGridError">Failed to load types: {typesError}</p>;

return(
<>
Expand All @@ -67,10 +80,10 @@ function ChoosePokemon(){
value={selectedGen}
onChange={handleGenChange}
>
<option value="">Select</option>
{gens.map((gens) => (
<option key={gens.label} value={gens.value}>
{gens.label}
<option value="">Any Gen</option>
{gens.map((gen) => (
<option key={gen.value} value={gen.value}>
{gen.label}
</option>
))}
</select>
Expand All @@ -86,18 +99,23 @@ function ChoosePokemon(){
</option>
))}
</select>

</>
);
}

export default function generateTeam(){
export default function generateTeam({ selectedGen, selectedType, onGenChange, onTypeChange }: ChoosePokemonProps){

return(
<>
<ChoosePokemon />
<ChoosePokemon
selectedGen={selectedGen}
selectedType={selectedType}
onGenChange={onGenChange}
onTypeChange={onTypeChange}
/>
<button>Generate Team</button>
</>

);
}
}
27 changes: 27 additions & 0 deletions PTG/src/PokemonGrid.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.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;
}

.pokemonCard img {
width: 100%;
max-width: 120px;
height: auto;
}

.pokemonGridError {
color: red;
text-align: center;
}
132 changes: 132 additions & 0 deletions PTG/src/PokemonGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { useEffect, useState } from "react";
import "./PokemonGrid.css";
import { fetchGenerationSpeciesIds } from "./generations";

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 = {
selectedGen: string;
selectedType: string;
};

export default function PokemonGrid({ selectedGen, 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 [genIds, setGenIds] = useState<Set<number> | null>(null);
const [genError, setGenError] = useState<string | 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;

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 (!selectedGen) {
setGenIds(null);
setGenError(null);
return;
}

fetchGenerationSpeciesIds(selectedGen)
.then((ids) => {
if (!cancelled) {
setGenIds(ids);
setGenError(null);
}
})
.catch((err: Error) => {
if (!cancelled) setGenError(err.message);
});

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

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 (genError) return <p className="pokemonGridError">Failed to load generation: {genError}</p>;

const filtered = pokemon.filter(({ id, name }) => {
if (genIds && !genIds.has(id)) return false;
if (typeNames && !typeNames.has(name)) return false;
return true;
});

return (
<div className="pokemonGrid">
{filtered.map(({ id, name, image }) => (
<div className="pokemonCard" key={id}>
<img src={image} alt={name} loading="lazy" />
<span>{capitalize(name)}</span>
</div>
))}
</div>
);
}
Loading