diff --git a/src/img/missingno.png b/src/img/missingno.png new file mode 100644 index 0000000..ceca1ca Binary files /dev/null and b/src/img/missingno.png differ diff --git a/src/scss/sprites.scss b/src/scss/sprites.scss index 3f72644..ae66d1c 100644 --- a/src/scss/sprites.scss +++ b/src/scss/sprites.scss @@ -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 diff --git a/src/ts/app/PokemonApp.tsx b/src/ts/app/PokemonApp.tsx index de1ce09..bcbb905 100644 --- a/src/ts/app/PokemonApp.tsx +++ b/src/ts/app/PokemonApp.tsx @@ -32,8 +32,10 @@ class PokemonApp extends React.Component { 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; + // } }