Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
26 changes: 22 additions & 4 deletions PTG/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
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 [selectedGeneration, setSelectedGeneration] = useState(() => loadStoredFilters().selectedGeneration);
const [selectedType, setSelectedType] = useState(() => loadStoredFilters().selectedType);

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

return(
<div>
<Header />
<ChoosePokemon />
<ChoosePokemon
selectedGame={selectedGame}
selectedGeneration={selectedGeneration}
selectedType={selectedType}
onGameChange={setSelectedGame}
onGenerationChange={setSelectedGeneration}
onTypeChange={setSelectedType}
/>
<PokemonGrid selectedGame={selectedGame} selectedGeneration={selectedGeneration} selectedType={selectedType} />
</div>

);
}
);
}
5 changes: 5 additions & 0 deletions PTG/src/ChoosePokemon.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.chooseType {
field-sizing: content;
width: fit-content;
max-width: 100%;
}
210 changes: 146 additions & 64 deletions PTG/src/ChoosePokemon.tsx
Original file line number Diff line number Diff line change
@@ -1,76 +1,150 @@
import {useState} from "react"
import { useEffect, useState } from "react";
import "./ChoosePokemon.css";
import { fetchGames, type GameOption } from "./games";
import { fetchGenerations, type GenerationOption } from "./generations";
import { fetchTypes, type TypeOption } from "./pokemonTypes";

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

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

// Earliest generation implied by the selected game and/or generation filter (-1 if neither is set).
function effectiveGenerationIndex(
selectedGame: string,
games: GameOption[],
selectedGeneration: string,
generations: GenerationOption[]
): number {
const indices: number[] = [];

const game = games.find((g) => g.value === selectedGame);
if (game) {
const gameGenIndex = generations.findIndex((gen) => gen.value === game.generation);
if (gameGenIndex !== -1) indices.push(gameGenIndex);
}

if (selectedGeneration) {
const generationIndex = generations.findIndex((gen) => gen.value === selectedGeneration);
if (generationIndex !== -1) indices.push(generationIndex);
}

return indices.length === 0 ? -1 : Math.min(...indices);
}

// Types introduced after the effective generation aren't offered yet.
function changeTypeArray(effectiveIndex: number, types: TypeOption[], generations: GenerationOption[]) {
const available = effectiveIndex === -1
? types
: types.filter((type) => {
const typeGenIndex = generations.findIndex((gen) => gen.value === type.generation);
return typeGenIndex === -1 || typeGenIndex <= effectiveIndex;
});
return [ANY_TYPE, ...available];
}

type ChoosePokemonProps = {
selectedGame: string;
selectedGeneration: string;
selectedType: string;
onGameChange: (value: string) => void;
onGenerationChange: (value: string) => void;
onTypeChange: (value: string) => void;
}

function ChoosePokemon({ selectedGame, selectedGeneration, selectedType, onGameChange, onGenerationChange, onTypeChange }: ChoosePokemonProps){
const [games, setGames] = useState<GameOption[]>([]);
const [gamesError, setGamesError] = useState<string | null>(null);
const [generations, setGenerations] = useState<GenerationOption[]>([]);
const [generationsError, setGenerationsError] = 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);
});

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

type Gen = {
label: string,
value: string
fetchGenerations()
.then((data) => {
if (!cancelled) setGenerations(data);
})
.catch((err: Error) => {
if (!cancelled) setGenerationsError(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<HTMLSelectElement>) {
const newGame = e.target.value;
onGameChange(newGame);
// Drop the type filter if it's no longer valid for the new game
const availableTypes = changeTypeArray(effectiveGenerationIndex(newGame, games, selectedGeneration, generations), types, generations);
if (!availableTypes.some((t) => t.value === selectedType)) {
onTypeChange('any');
}
}

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);
function handleGenerationChange(e: React.ChangeEvent<HTMLSelectElement>) {
const newGeneration = e.target.value;
onGenerationChange(newGeneration);
// Drop the type filter if it's no longer valid for the new generation
const availableTypes = changeTypeArray(effectiveGenerationIndex(selectedGame, games, newGeneration, generations), types, generations);
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 : TypeChoice[] = changeTypeArray(effectiveGenerationIndex(selectedGame, games, selectedGeneration, generations), types, generations);

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

return(
<>
<select
className="chooseType"
value={selectedGen}
onChange={handleGenChange}
value={selectedGame}
onChange={handleGameChange}
>
<option value="">Select</option>
{gens.map((gens) => (
<option key={gens.label} value={gens.value}>
{gens.label}
<option value="">Any Game</option>
{games.map((game) => (
<option key={game.value} value={game.value}>
{game.label}
</option>
))}
</select>
<select
className="chooseType"
value={selectedGeneration}
onChange={handleGenerationChange}
>
<option value="">Any Gen</option>
{generations.map((generation) => (
<option key={generation.value} value={generation.value}>
{generation.label}
</option>
))}
</select>
Expand All @@ -86,18 +160,26 @@ function ChoosePokemon(){
</option>
))}
</select>

</>
);
}

export default function generateTeam(){

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

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

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