add search filter to pokemon list
This commit is contained in:
parent
83bf78fee4
commit
44cf0e349c
BIN
src/img/missingno.png
Normal file
BIN
src/img/missingno.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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,46 +48,86 @@ 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 }>
|
||||
<Measure
|
||||
bounds={ true }
|
||||
onResize={ onResize }
|
||||
>
|
||||
{
|
||||
({ measureRef }) => (
|
||||
<div ref={ measureRef }>
|
||||
<VariableSizeList
|
||||
height={ height }
|
||||
itemCount={ this.props.pokemonList.length }
|
||||
estimatedItemSize={ 25 }
|
||||
itemSize={ this.calculateRowHeight }
|
||||
width={ width }
|
||||
>
|
||||
{ this.rowFactory.bind(this) }
|
||||
</VariableSizeList>
|
||||
</div>
|
||||
)
|
||||
<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 }
|
||||
>
|
||||
{
|
||||
({ measureRef }) => (
|
||||
<div ref={ measureRef }>
|
||||
<VariableSizeList
|
||||
ref={ this.setListRef }
|
||||
height={ height }
|
||||
itemKey={ this.getListItemKey }
|
||||
itemCount={ listLength }
|
||||
estimatedItemSize={ 25 }
|
||||
itemSize={ this.calculateRowHeight }
|
||||
width={ width }
|
||||
>
|
||||
{ this.rowFactory.bind(this) }
|
||||
</VariableSizeList>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Measure>
|
||||
}
|
||||
</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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -4,10 +4,28 @@
|
||||
height: 100vh;
|
||||
font-size: 0.8rem;
|
||||
flex-basis: 20em;
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
margin-left: 1rem;
|
||||
|
||||
& > * {
|
||||
height: 100%;
|
||||
.listWrapper {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
padding: 6px;
|
||||
|
||||
& > * {
|
||||
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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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',
|
||||
};
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
align-items: stretch;
|
||||
height: 100vh;
|
||||
|
||||
& > * {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
// & > * {
|
||||
// flex-grow: 0;
|
||||
// flex-shrink: 0;
|
||||
// }
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user