add search filter to pokemon list

This commit is contained in:
Jeff Colombo 2019-02-02 01:35:09 -05:00
parent 83bf78fee4
commit 44cf0e349c
10 changed files with 191 additions and 52 deletions

BIN
src/img/missingno.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,5 +1,17 @@
$scale: 4;
.pokemon-missing-no {
display: inline-block;
background: url('../img/missingno.png') no-repeat;
width: 84px;
height: 195px;
image-rendering: -webkit-crisp-edges;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
image-rendering: pixelated;
-ms-interpolation-mode: nearest-neighbor;
}
// generated by https://css.spritegen.com/
// sprites from https://pokemondb.net/sprites
// alolan sprites from https://www.pokecommunity.com/showthread.php?t=368703

View File

@ -32,8 +32,10 @@ class PokemonApp extends React.Component<IConnectedPokemonAppProps> {
public render() {
const {
activePokemonIndex,
activePokemonId,
pokemonList,
pokemonListFiltered,
filterTerm,
} = this.props.pokemonSelectListState;
const {
individualValues,
@ -44,9 +46,11 @@ class PokemonApp extends React.Component<IConnectedPokemonAppProps> {
<div className={ styles.wrapper }>
<PokemonSelectList
isLoading={ this.props.pokemonSelectListState.isLoading }
activePokemonIndex={ activePokemonIndex }
pokemonList={ pokemonList }
activePokemonId={ activePokemonId }
pokemonList={ filterTerm === '' ? pokemonList : pokemonListFiltered }
filterTerm={ this.props.pokemonSelectListState.filterTerm }
handleActivatePokemon={ this.handleActivatePokemon }
handleChangeFilter={ this.handleChangeFilter }
/>
{ leaguePokemon !== null &&
<PokemonExplorer
@ -60,13 +64,12 @@ class PokemonApp extends React.Component<IConnectedPokemonAppProps> {
);
}
private readonly handleActivatePokemon = (pokemonIndex : number) => {
const { dispatch, pokemonSelectListState } = this.props;
const pokemonId = pokemonSelectListState.pokemonList[pokemonIndex].id;
private readonly handleActivatePokemon = (pokemonId : string) => {
const { dispatch } = this.props;
dispatch(ActionsPokemonSelectList.fetchPokemonLeagueStats(pokemonId))
.then((leaguePokemon) => {
dispatch(ActionsPokemonSelectList.setActivePokemonIndex(pokemonIndex));
dispatch(ActionsPokemonSelectList.setActivePokemonId(pokemonId));
dispatch(ActionsPokemonExplorer.setIvLevel(null));
dispatch(ActionsPokemonExplorer.setIvHp(null));
dispatch(ActionsPokemonExplorer.setIvAtk(null));
@ -81,6 +84,10 @@ class PokemonApp extends React.Component<IConnectedPokemonAppProps> {
.then(() => dispatch(ActionsPokemonExplorer.setIsLoading(false)));
}
private readonly handleChangeFilter = (filterTerm : string) => {
return this.props.dispatch(ActionsPokemonSelectList.filterPokemonList(filterTerm));
}
private readonly handleChangeIndividualValue = (stat : IndividualValueKey, value : number | null) => {
const { dispatch } = this.props;

View File

@ -12,10 +12,12 @@ import * as styles from './styles/PokemonSelectList.scss';
export interface IPokemonSelectListProps {
isLoading : boolean;
activePokemonIndex : number | null;
activePokemonId : string | null;
pokemonList : Array<IPokemon>;
filterTerm : string;
handleActivatePokemon : (index : number) => void;
handleActivatePokemon : (pokemonId : string) => void;
handleChangeFilter : (filterTerm : string) => Promise<void>;
}
interface IState {
@ -31,6 +33,7 @@ interface IRowFactory {
}
export class PokemonSelectList extends React.Component<IPokemonSelectListProps, IState> {
private listRef : VariableSizeList | null = null;
constructor(props : IPokemonSelectListProps) {
super(props);
@ -45,21 +48,43 @@ export class PokemonSelectList extends React.Component<IPokemonSelectListProps,
public render() {
const { width, height } = this.state.dimensions;
const listLength = this.props.pokemonList.length;
const onResize = (contentRect : ContentRect) => {
if (typeof contentRect.bounds !== 'undefined') {
this.setState({ dimensions: contentRect.bounds });
}
};
const classes = classNames(
'nes-container',
const wrapperCss = classNames(
styles.leftPanel,
{
loading: this.props.isLoading,
}
);
const listWrapperCss = classNames(
'nes-container',
styles.listWrapper,
{
[ styles.emptyList ]: listLength === 0
}
);
const inputTextCss = classNames(
'nes-input',
styles.filterInput
);
return (
<div id="pokemon-select-list" className={ classes }>
<div id="pokemon-select-list" className={ wrapperCss }>
<input
name="filter"
type="text"
className={ inputTextCss }
onChange={ this.handleChangeFilter }
value={ this.props.filterTerm }
placeholder="Pokemon Name"
/>
<div className={ listWrapperCss }>
{ listLength > 0 &&
<Measure
bounds={ true }
onResize={ onResize }
@ -68,8 +93,10 @@ export class PokemonSelectList extends React.Component<IPokemonSelectListProps,
({ measureRef }) => (
<div ref={ measureRef }>
<VariableSizeList
ref={ this.setListRef }
height={ height }
itemCount={ this.props.pokemonList.length }
itemKey={ this.getListItemKey }
itemCount={ listLength }
estimatedItemSize={ 25 }
itemSize={ this.calculateRowHeight }
width={ width }
@ -80,11 +107,27 @@ export class PokemonSelectList extends React.Component<IPokemonSelectListProps,
)
}
</Measure>
}
{ listLength === 0 &&
<div className={ styles.emptyState }>
<i className="pokemon-missing-no" />
<h3>MissingNo.</h3>
</div>
}
</div>
</div>
);
}
private readonly calculateRowHeight = (index : number) => this.props.pokemonList[index].form !== null ? 40 : 25;
private readonly setListRef = (element : VariableSizeList) => this.listRef = element;
private readonly getListItemKey = (index : number) => {
return index + this.props.pokemonList[index].id;
}
private readonly calculateRowHeight = (index : number) => {
return this.props.pokemonList[index].form !== null ? 40 : 25;
}
private rowFactory({ index, style } : IRowFactory) {
const pokemon = this.props.pokemonList[index];
@ -92,7 +135,7 @@ export class PokemonSelectList extends React.Component<IPokemonSelectListProps,
const anchorCss = classNames(
'list-item',
{
active: this.props.activePokemonIndex === index
active: this.props.activePokemonId === pokemon.id
}
);
const dexCss = classNames(
@ -103,10 +146,10 @@ export class PokemonSelectList extends React.Component<IPokemonSelectListProps,
'de-emphasize',
styles.form
);
const onClick = () => this.props.handleActivatePokemon(index);
const onClick = () => this.props.handleActivatePokemon(pokemon.id);
return (
<a
key={ index }
key={ index + pokemon.id }
style={ style }
className={ anchorCss }
onClick={ onClick }
@ -119,4 +162,13 @@ export class PokemonSelectList extends React.Component<IPokemonSelectListProps,
</a>
);
}
private readonly handleChangeFilter = (event : React.ChangeEvent<HTMLInputElement>) => {
this.props.handleChangeFilter(event.currentTarget.value)
.then(() => {
if (this.listRef !== null) {
this.listRef.resetAfterIndex(0, true);
}
});
}
}

View File

@ -9,10 +9,37 @@ export const setIsLoading = (isLoading : boolean) => action(PokemonSelectListAct
export const setPokemonList = (pokemonList : Array<IPokemon>) => action(PokemonSelectListActionTypes.SET_POKEMON_LIST, { pokemonList });
export const setActivePokemonIndex = (activePokemonIndex : number | null) => action(PokemonSelectListActionTypes.SET_ACTIVE_POKEMON_INDEX, { activePokemonIndex });
export const setPokemonListFiltered = (filterTerm : string, pokemonListFiltered : Array<IPokemon>) => action(PokemonSelectListActionTypes.SET_POKEMON_LIST_FILTERED, { filterTerm, pokemonListFiltered });
export const setActivePokemonId = (activePokemonId : string | null) => action(PokemonSelectListActionTypes.SET_ACTIVE_POKEMON_ID, { activePokemonId });
export const setPokemonLeagueStats = (pokemonId : string, pokemonLeagueStats : ILeaguePokemon) => action(PokemonSelectListActionTypes.SET_POKEMON_LEAGUE_STATS, { pokemonId, pokemonLeagueStats });
export const filterPokemonList = (
filterTerm : string
) : ThunkResult<Promise<void>> => {
return async (dispatch, getState, extraArguments) => {
let pokemonListFiltered : Array<IPokemon> = [];
if (filterTerm !== '') {
const pokemonList = getState().pokemonSelectListState.pokemonList;
const normalizedFilterTerm = filterTerm.toLowerCase();
pokemonListFiltered = pokemonList.reduce((result : Array<IPokemon>, pokemon) => {
const pokemonName = pokemon.name.toLowerCase();
const pokemonDex = '' + pokemon.dex;
const pokemonForm = (pokemon.form || '').toLowerCase();
if (pokemonName.indexOf(normalizedFilterTerm) === 0 ||
pokemonDex.indexOf(normalizedFilterTerm) === 0 ||
normalizedFilterTerm === pokemonForm
) {
result.push(pokemon);
}
return result;
}, []);
}
dispatch(setPokemonListFiltered(filterTerm, pokemonListFiltered));
};
};
export const fetchPokemonList = (
) : ThunkResult<Promise<void>> => {
return async (dispatch, getState, extraArguments) => {

View File

@ -5,10 +5,11 @@ import { IPokemonSelectListState, PokemonSelectListActionTypes } from './types';
export const initialState : IPokemonSelectListState = {
isLoading: true,
activePokemonIndex: null,
activePokemonId: null,
pokemonList: [],
pokemonListFiltered: [],
pokemonLeagueStats: {}
filterTerm: '',
pokemonLeagueStats: {},
};
const reduceSetIsLoading = (
@ -27,12 +28,21 @@ const reduceSetPokemonList = (
pokemonList: action.payload.pokemonList,
});
const reduceSetActivePokemonIndex = (
const reduceSetPokemonListFiltered = (
state : IPokemonSelectListState,
action : ReturnType<typeof Actions.setActivePokemonIndex>
action : ReturnType<typeof Actions.setPokemonListFiltered>
) : IPokemonSelectListState => ({
...state,
activePokemonIndex: action.payload.activePokemonIndex,
filterTerm: action.payload.filterTerm,
pokemonListFiltered: action.payload.pokemonListFiltered,
});
const reduceSetActivePokemonId = (
state : IPokemonSelectListState,
action : ReturnType<typeof Actions.setActivePokemonId>
) : IPokemonSelectListState => ({
...state,
activePokemonId: action.payload.activePokemonId,
});
const reduceSetPokemonLeagueStats = (
@ -55,8 +65,10 @@ export const PokemonSelectListReducers : Reducer<IPokemonSelectListState> = (
return reduceSetIsLoading(state, action as ReturnType<typeof Actions.setIsLoading>);
case PokemonSelectListActionTypes.SET_POKEMON_LIST:
return reduceSetPokemonList(state, action as ReturnType<typeof Actions.setPokemonList>);
case PokemonSelectListActionTypes.SET_ACTIVE_POKEMON_INDEX:
return reduceSetActivePokemonIndex(state, action as ReturnType<typeof Actions.setActivePokemonIndex>);
case PokemonSelectListActionTypes.SET_POKEMON_LIST_FILTERED:
return reduceSetPokemonListFiltered(state, action as ReturnType<typeof Actions.setPokemonListFiltered>);
case PokemonSelectListActionTypes.SET_ACTIVE_POKEMON_ID:
return reduceSetActivePokemonId(state, action as ReturnType<typeof Actions.setActivePokemonId>);
case PokemonSelectListActionTypes.SET_POKEMON_LEAGUE_STATS:
return reduceSetPokemonLeagueStats(state, action as ReturnType<typeof Actions.setPokemonLeagueStats>);
default:

View File

@ -4,10 +4,28 @@
height: 100vh;
font-size: 0.8rem;
flex-basis: 20em;
display: flex;
flex-flow: column nowrap;
margin-left: 1rem;
.listWrapper {
flex: 1 1 auto;
display: flex;
padding: 6px;
& > * {
height: 100%;
width: 100%;
}
&.emptyList .emptyState {
align-self: center;
text-align: center;
margin-top: -100%;
& > *:first-child {
margin: 1em auto;
}
}
}
a {
@ -18,6 +36,11 @@
}
}
.filterInput {
margin-left: 0;
margin-right: 0;
}
.dex,
.form {
font-size: 0.8em;

View File

@ -1,5 +1,9 @@
// This file is automatically generated.
// Please do not change this file!
export const dex: string;
export const emptyList: string;
export const emptyState: string;
export const filterInput: string;
export const form: string;
export const leftPanel: string;
export const listWrapper: string;

View File

@ -2,15 +2,17 @@ import { ILeaguePokemon, IPokemon } from 'app/models/Pokemon';
export interface IPokemonSelectListState {
isLoading : boolean;
activePokemonIndex : number | null;
activePokemonId : string | null;
pokemonList : Array<IPokemon>;
pokemonListFiltered : Array<IPokemon>;
filterTerm : string;
pokemonLeagueStats : { [id : string] : ILeaguePokemon };
}
export const PokemonSelectListActionTypes = {
SET_IS_LOADING: 'POKEMON_SELECT_LIST/SET_IS_LOADING',
SET_POKEMON_LIST: 'POKEMON_SELECT_LIST/SET_POKEMON_LIST',
SET_ACTIVE_POKEMON_INDEX: 'POKEMON_SELECT_LIST/SET_ACTIVE_POKEMON_INDEX',
SET_POKEMON_LIST_FILTERED: 'POKEMON_SELECT_LIST/SET_POKEMON_LIST_FILTERED',
SET_ACTIVE_POKEMON_ID: 'POKEMON_SELECT_LIST/SET_ACTIVE_POKEMON_ID',
SET_POKEMON_LEAGUE_STATS: 'POKEMON_SELECT_LIST/SET_POKEMON_LEAGUE_STATS',
};

View File

@ -6,8 +6,8 @@
align-items: stretch;
height: 100vh;
& > * {
flex-grow: 0;
flex-shrink: 0;
}
// & > * {
// flex-grow: 0;
// flex-shrink: 0;
// }
}