From 44cf0e349c9d1261c33cc395b27c9f7645825bb7 Mon Sep 17 00:00:00 2001 From: Jeff Colombo Date: Sat, 2 Feb 2019 01:35:09 -0500 Subject: [PATCH] add search filter to pokemon list --- src/img/missingno.png | Bin 0 -> 1452 bytes src/scss/sprites.scss | 12 ++ src/ts/app/PokemonApp.tsx | 21 ++-- .../PokemonSelectList/PokemonSelectList.tsx | 108 +++++++++++++----- .../components/PokemonSelectList/actions.ts | 29 ++++- .../components/PokemonSelectList/reducers.ts | 26 +++-- .../styles/PokemonSelectList.scss | 29 ++++- .../styles/PokemonSelectList.scss.d.ts | 4 + .../app/components/PokemonSelectList/types.ts | 6 +- src/ts/app/styles/PokemonApp.scss | 8 +- 10 files changed, 191 insertions(+), 52 deletions(-) create mode 100644 src/img/missingno.png diff --git a/src/img/missingno.png b/src/img/missingno.png new file mode 100644 index 0000000000000000000000000000000000000000..ceca1cac56cfd72e2806cddb1ad45815a7c607ef GIT binary patch literal 1452 zcmX9;3p^8A99{|;Zsk=&T({dU<9fdn=9QUgW)iM+m53o*VWeCkB+Ijq*)SoGGKyOC za6RS{Z6=e@QkZV{dghwDd*=Sm@0|1dedqE0&-efSCkyL!OcAOMg+L&RD0deenDL;$ z+ARY{&E1x^V3G+zA9I0-C5`s%UI9SlF1Y)XAP{+FN#6k}y?X$FvKLVpS6P9Sl*~cd zdH2Z(2t?Wj<$}Z~@V}HJqkWJH8Nt~2FR3q8+fjSGv!Zq-r8%;t5Z#qZN)L`LsJ+l3 zDmP*0DmQzf=CR6Sg15`?)H_X22Va$VC~k6idnpo zJ)WRjcztQ6aOx-0(phhdeCVF^r2~ar65`|;A9)*q*e7Je6soenE2|IM=p z%wK)`%pF8V(@=EA9M|V}TYE2DRO-@M51o>Hmy#X*WBwJRE4U(4j|{jl5yaI?>mK%G zk^c@|>C3D8H7fPFGu^JxFO)O6z**ZnJ&9e$jZGNl0xAU$g($tU7D0KjQzCLfZkEo1 z1)vk$n)76u%_|?vvCm+K(F$A3%ONqSD&MVHI89C5)Y&zWlx(NMb|zc+dN_GJ&lf)Gju6%<1MtkSKb?$j&vD@W zJIJu!7oQkiU}v#KC*D-LEu7Sg*a(u`MBu0JCh*Ml=)s6qgWgSh$vF_s!hB;>AVuJE zQ}^83L687L?uw;0a+YB zTZ}mc<8Xq|+cGWZWB86KY7f}jF$RBF6Ax9ys>9gYw+Pu!7Q6Kw*q${t&k5-U#!1Hq zV)MTP{0}ge{Slr$&&r;Le0tP=KD{lNxtg*cgck3$LC8jD_wB6bce30fg;zS`Y`>E@ zLS<3IInV&-arozRk$ORxhOQHEN?0`&Uq%znoYt9;f42E?WT`%bs@_*fvwKb}F~Mt* z`rTdIzE!Pix!$!dt_xe6mf9vDcc4B;i8;0EvEx~OJ>1qbv%{JoB;z8&Qcq*%okn`i z$P4{4cGnI-E9&J=&`cY4DlW8<3`=A6nv)9`mmGl58o-WZD#N!m)@^~hxD(qi0Er=!74zb@`0K3_WHuuLqL9Kvem5FN6Px63z0gS+m(KxwB!vYxse>l&H*p$jOBhP?NQ+Sk8uL_ zR5_xpfo#h~7;n|h%p)ddZsJ6-HqcnK6F{515nL@CpTI>aP|A_$r5G{Gb_T2a?p { public render() { const { - activePokemonIndex, + activePokemonId, pokemonList, + pokemonListFiltered, + filterTerm, } = this.props.pokemonSelectListState; const { individualValues, @@ -44,9 +46,11 @@ class PokemonApp extends React.Component {
{ leaguePokemon !== null && { ); } - 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 { .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; diff --git a/src/ts/app/components/PokemonSelectList/PokemonSelectList.tsx b/src/ts/app/components/PokemonSelectList/PokemonSelectList.tsx index ab7c98a..a96c2d6 100644 --- a/src/ts/app/components/PokemonSelectList/PokemonSelectList.tsx +++ b/src/ts/app/components/PokemonSelectList/PokemonSelectList.tsx @@ -12,10 +12,12 @@ import * as styles from './styles/PokemonSelectList.scss'; export interface IPokemonSelectListProps { isLoading : boolean; - activePokemonIndex : number | null; + activePokemonId : string | null; pokemonList : Array; + filterTerm : string; - handleActivatePokemon : (index : number) => void; + handleActivatePokemon : (pokemonId : string) => void; + handleChangeFilter : (filterTerm : string) => Promise; } interface IState { @@ -31,6 +33,7 @@ interface IRowFactory { } export class PokemonSelectList extends React.Component { + private listRef : VariableSizeList | null = null; constructor(props : IPokemonSelectListProps) { super(props); @@ -45,46 +48,86 @@ export class PokemonSelectList extends React.Component { 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 ( -
- - { - ({ measureRef }) => ( -
- - { this.rowFactory.bind(this) } - -
- ) +
+ +
+ { listLength > 0 && + + { + ({ measureRef }) => ( +
+ + { this.rowFactory.bind(this) } + +
+ ) + } +
} - + { listLength === 0 && +
+ +

MissingNo.

+
+ } +
); } - 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 this.props.handleActivatePokemon(index); + const onClick = () => this.props.handleActivatePokemon(pokemon.id); return ( ); } + + private readonly handleChangeFilter = (event : React.ChangeEvent) => { + this.props.handleChangeFilter(event.currentTarget.value) + .then(() => { + if (this.listRef !== null) { + this.listRef.resetAfterIndex(0, true); + } + }); + } } diff --git a/src/ts/app/components/PokemonSelectList/actions.ts b/src/ts/app/components/PokemonSelectList/actions.ts index c02832a..8cb5a3d 100644 --- a/src/ts/app/components/PokemonSelectList/actions.ts +++ b/src/ts/app/components/PokemonSelectList/actions.ts @@ -9,10 +9,37 @@ export const setIsLoading = (isLoading : boolean) => action(PokemonSelectListAct export const setPokemonList = (pokemonList : Array) => 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) => 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> => { + return async (dispatch, getState, extraArguments) => { + let pokemonListFiltered : Array = []; + if (filterTerm !== '') { + const pokemonList = getState().pokemonSelectListState.pokemonList; + const normalizedFilterTerm = filterTerm.toLowerCase(); + pokemonListFiltered = pokemonList.reduce((result : Array, 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> => { return async (dispatch, getState, extraArguments) => { diff --git a/src/ts/app/components/PokemonSelectList/reducers.ts b/src/ts/app/components/PokemonSelectList/reducers.ts index f821fd4..86ded1e 100644 --- a/src/ts/app/components/PokemonSelectList/reducers.ts +++ b/src/ts/app/components/PokemonSelectList/reducers.ts @@ -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 + action : ReturnType ) : IPokemonSelectListState => ({ ...state, - activePokemonIndex: action.payload.activePokemonIndex, + filterTerm: action.payload.filterTerm, + pokemonListFiltered: action.payload.pokemonListFiltered, +}); + +const reduceSetActivePokemonId = ( + state : IPokemonSelectListState, + action : ReturnType +) : IPokemonSelectListState => ({ + ...state, + activePokemonId: action.payload.activePokemonId, }); const reduceSetPokemonLeagueStats = ( @@ -55,8 +65,10 @@ export const PokemonSelectListReducers : Reducer = ( return reduceSetIsLoading(state, action as ReturnType); case PokemonSelectListActionTypes.SET_POKEMON_LIST: return reduceSetPokemonList(state, action as ReturnType); - case PokemonSelectListActionTypes.SET_ACTIVE_POKEMON_INDEX: - return reduceSetActivePokemonIndex(state, action as ReturnType); + case PokemonSelectListActionTypes.SET_POKEMON_LIST_FILTERED: + return reduceSetPokemonListFiltered(state, action as ReturnType); + case PokemonSelectListActionTypes.SET_ACTIVE_POKEMON_ID: + return reduceSetActivePokemonId(state, action as ReturnType); case PokemonSelectListActionTypes.SET_POKEMON_LEAGUE_STATS: return reduceSetPokemonLeagueStats(state, action as ReturnType); default: diff --git a/src/ts/app/components/PokemonSelectList/styles/PokemonSelectList.scss b/src/ts/app/components/PokemonSelectList/styles/PokemonSelectList.scss index 13fc59c..206252a 100644 --- a/src/ts/app/components/PokemonSelectList/styles/PokemonSelectList.scss +++ b/src/ts/app/components/PokemonSelectList/styles/PokemonSelectList.scss @@ -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; diff --git a/src/ts/app/components/PokemonSelectList/styles/PokemonSelectList.scss.d.ts b/src/ts/app/components/PokemonSelectList/styles/PokemonSelectList.scss.d.ts index c8db241..58e3d96 100644 --- a/src/ts/app/components/PokemonSelectList/styles/PokemonSelectList.scss.d.ts +++ b/src/ts/app/components/PokemonSelectList/styles/PokemonSelectList.scss.d.ts @@ -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; diff --git a/src/ts/app/components/PokemonSelectList/types.ts b/src/ts/app/components/PokemonSelectList/types.ts index 053cf11..86c7d4c 100644 --- a/src/ts/app/components/PokemonSelectList/types.ts +++ b/src/ts/app/components/PokemonSelectList/types.ts @@ -2,15 +2,17 @@ import { ILeaguePokemon, IPokemon } from 'app/models/Pokemon'; export interface IPokemonSelectListState { isLoading : boolean; - activePokemonIndex : number | null; + activePokemonId : string | null; pokemonList : Array; pokemonListFiltered : Array; + 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', }; diff --git a/src/ts/app/styles/PokemonApp.scss b/src/ts/app/styles/PokemonApp.scss index 9e8ddf4..9c347ba 100644 --- a/src/ts/app/styles/PokemonApp.scss +++ b/src/ts/app/styles/PokemonApp.scss @@ -6,8 +6,8 @@ align-items: stretch; height: 100vh; - & > * { - flex-grow: 0; - flex-shrink: 0; - } + // & > * { + // flex-grow: 0; + // flex-shrink: 0; + // } }