Skip to content

Commit

Permalink
feat: ✨ pokemon filter
Browse files Browse the repository at this point in the history
add filter by criteria and by characteristics
  • Loading branch information
Isaac-cura committed Feb 22, 2023
1 parent 875ab8f commit bc02be6
Show file tree
Hide file tree
Showing 11 changed files with 519 additions and 45 deletions.
Binary file added public/assets/loading.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/pikachu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion src/components/list-header.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@
<script setup lang="ts">
import { IonHeader, IonToolbar, IonTitle, IonInput, IonIcon, IonButton, IonButtons, IonItem } from '@ionic/vue';
import { search, close } from 'ionicons/icons';
import { ref } from 'vue';
import { ref, watch } from 'vue';
const emit = defineEmits(["change", "toggle"])
const props = defineProps<{pokemonName: string}>()
const input = ref();
const criteria = ref("");
const emitChange = (event: CustomEvent) => {
Expand All @@ -49,6 +50,10 @@ const focusInput = () => {
const emitToggle = () => {
emit("toggle")
}
watch(() => props.pokemonName, () => {
criteria.value = props.pokemonName
})
</script>
<style scoped>
ion-header {
Expand Down
66 changes: 64 additions & 2 deletions src/components/pokemon-moves.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,67 @@
<template>
<div data-test="pokemon-moves"></div>
<h2>
{{ moveLengthLabel }}
</h2>
<div class="pokemon-moves" data-test="pokemon-moves">

<div class="pokemon-move" :class="move.type" v-for="(move, i) of moves" :key="i">
<ion-icon :src="move.icon"></ion-icon>
<div>
{{ move.move }}
</div>

</div>
</div>
</template>
<script setup lang="ts">
</script>
import { pokemonTypeIcon } from '@/constants/types-icons';
import { Pokemon } from '@/models/pokemon.model';
import { computed } from 'vue';
const props = defineProps<{ pokemon: Pokemon }>()
const moves = computed(() => {
const typesLength = props.pokemon.types.length;
return props.pokemon.moves.map((move, i) => ({
move,
icon: pokemonTypeIcon(props.pokemon.types[i % typesLength]),
type: props.pokemon.types[i % typesLength]
}))
})
const moveLengthLabel = computed(() => {
const movesLegth = props.pokemon.moves.length
return movesLegth == 1
? "1 move"
: `${movesLegth} moves`
})
</script>
<style scoped>
.pokemon-moves {
display: flex;
flex-wrap: wrap;
gap: 30px;
margin-top: 15px;
}
.pokemon-move ion-icon {
border-radius: 100%;
background: white;
margin-top: 7px;
font-size: 15px;
padding: 5px;
}
.pokemon-move div {
overflow: hidden;
text-overflow: ellipsis;
padding: 0px 10px;
white-space: nowrap;
}
.pokemon-move {
height: 64px;
width: 100px;
border-radius: 4px;
text-align: center;
color: var(--neutral-50);
text-overflow: ellipsis;
overflow: hidden;
}
</style>
116 changes: 105 additions & 11 deletions src/modals/pokemon-filters.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,119 @@
<template>
<ion-modal ref="modal" trigger="open-modal">
<ion-page>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="cancel()">Cancel</ion-button>
</ion-buttons>
<ion-title>Welcome</ion-title>
<ion-buttons slot="end">
<ion-button :strong="true" @click="confirm()">Confirm</ion-button>
<ion-button @click="closeModal">
<ion-icon class="primary" :icon="close"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item>
<ion-label position="stacked">Enter your name</ion-label>
<ion-input ref="input" type="text" placeholder="Your name"></ion-input>
<ion-select v-model="filters.moves" placeholder="Select movement number">
<ion-select-option v-for="(move, i) of props.moves " :value="move" :key="i">
{{ move }}
</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-select v-model="filters.experience" placeholder="Select experience level">
<ion-select-option v-for="(experience, i) of props.experiences " :value="experience" :key="i">
{{ experience }}
</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-select :value="filters.types" placeholder="Pokemon type" :multiple="true" @ion-change="changes">
<ion-select-option v-for="(type, i) of props.types" :value="type" :key="i">
{{ type }}
</ion-select-option>
</ion-select>
</ion-item>
</ion-content>
</ion-modal>
<ion-footer>
<ion-button @click="closeModal" color="medium" class="cancel">cancel</ion-button>
<ion-button @click="submitModal">confirm</ion-button>
</ion-footer>
</ion-page>
</template>
<script setup lang="ts">
const cancel = () => {return 5}
const confirm = () => {return 7}
import { IonFooter, IonIcon, IonSelect, IonSelectOption, IonPage, IonHeader, IonToolbar, IonButtons, IonButton, IonContent, IonItem, modalController } from '@ionic/vue';
import { close } from 'ionicons/icons';
import { onMounted, reactive, ref } from 'vue';
const moves = ref()
const props = defineProps<{
filters: {
types: string[],
experience: number,
moves: number
},
moves: number[],
types: string[],
experiences: number[]
}>()
const filters = reactive(Object.assign({}, props.filters))
const changes = (event: CustomEvent) => {
filters.types = event.detail.value
}
onMounted(() => {
console.log(props)
modalController.getTop
moves.value = "oranges"
})
const closeModal = () => {
modalController.dismiss()
}
const submitModal = () => {
modalController.dismiss(filters)
}
</script>
<style scoped>
ion-page {
--ion-background-color: var(--neutral-50)
}
ion-header {
background-color: var(--neutral-50);
}
ion-footer {
display: flex;
background-color: var(--neutral-50);
}
ion-button {
flex: 1
}
ion-header::after {
display: none;
}
.cancel {
--background-color: red;
}
ion-toolbar {
--ion-toolbar-background: var(--neutral-50);
}
ion-item {
--ion-background-color: var(--neutral-100);
--ion-background-color: var(--neutral-100);
--border-radius: 8px 8px 0 0;
margin-bottom: 24px;
}
ion-select {
--placeholder-color: var(--neutral-500);
--placeholder-opacity: 1;
width: 100%;
}</style>
21 changes: 19 additions & 2 deletions src/services/pokemon.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AsyncEither, Either, Failure, Success } from "@/models/either";
import { Pokemon, transfomPokemonDTO } from "@/models/pokemon.model";

export class PokemonService {

private controller?: AbortController;
constructor(private axios: Axios) { }

async getAll(limit: number, offset: number): AsyncEither<undefined, PokemonList> {
Expand Down Expand Up @@ -33,11 +33,28 @@ export class PokemonService {
}

async getByName(name: string, cancelable?: boolean): AsyncEither<undefined, Pokemon> {
const controller = new AbortController();
if(cancelable) {
this.controller?.abort();
this.controller = controller;
}
try {
const response = await this.axios.get(apiPaths.getPokemonByName(name))
await delay()
const response = await this.axios.get(apiPaths.getPokemonByName(name), {
signal: controller.signal
})
return Success.create(transfomPokemonDTO(response.data))
} catch {
return Failure.create()
}
}
}

/**programatic delay to prevent flashings with the loadings */
const delay = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(undefined)
}, 250)
})
}
53 changes: 44 additions & 9 deletions src/stores/pokemon-list.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,41 @@ export const usePokemonListStore = defineStore("pokemon-list", {
error: false
}),
getters: {
pokemonResultCount: (state): number => {
const pokemonListStore = usePokemonListStore()
return pokemonListStore.pokemonList.length
},
thereAreFilters: (state): boolean => {
const { types, experience, moves } = state.filters
return types.length > 0 || typeof experience !== "undefined" || typeof moves !== "undefined"
},
pokemonList: (state): Pokemon[] => {
const criteria = state.filters.criteria;
if(criteria) {
return [state.searchedPokemon].filter((pokemon):pokemon is Pokemon => pokemon?.name === criteria)
if (criteria) {
return [state.searchedPokemon].filter((pokemon): pokemon is Pokemon => pokemon?.name === criteria)
}
const pokemons = Object.values(state.pokemons);
return [filterByExperience, filterByTypes, filterByMoves]
.reduce((pokemons, filter) => filter(pokemons), pokemons)

},
filtersData: (state) => {
const pokemons = Object.values(state.pokemons);
const moves = pokemons.reduce((moves: number[], pokemon: Pokemon) => [...moves, pokemon.moves.length], [])
const experiences = pokemons.reduce((experiences: number[], pokemon: Pokemon) => [...experiences, pokemon.experience], [])
const types = pokemons.reduce((types: string[], pokemon: Pokemon) => [...types, ...pokemon.types], [])
return {
filters: state.filters,
experiences: [...(new Set(experiences))],
moves: [...(new Set(moves))],
types: [...(new Set(types))],

}
}
},

actions: {
async fetchAndUpdatePokemons(paginatorInfo: PaginatorDataSource) {
clearFiltersAndErrors()
const { limit, offset } = paginatorInfo;
this.loading = true;
const pokemonsResponse = await this.$pokemonService.getAll(limit ?? 0, offset ?? 0)
Expand All @@ -48,15 +69,14 @@ export const usePokemonListStore = defineStore("pokemon-list", {
}
},
async fetchPokemonByName(pokemonName: string) {

clearFiltersAndErrors()
this.filters.criteria = pokemonName

const pokemon = pokemonFromList(pokemonName)
if(typeof pokemon === "undefined") {
if (typeof pokemon === "undefined" && pokemonName) {
this.searchedPokemon = undefined;
this.loading = true
const response = await this.$pokemonService.getByName(pokemonName)
if(response.succeeded) {
if (response.succeeded) {
this.searchedPokemon = response.result
setSuccessRequest()
} else {
Expand All @@ -65,18 +85,33 @@ export const usePokemonListStore = defineStore("pokemon-list", {
} else {
this.searchedPokemon = pokemon;
}
},
setFilters(filters: { moves: number, types: string[], experience: number }) {
this.filters = {
...this.filters,
moves: filters.moves,
types: filters.types,
experience: filters.experience
}
}

}
})


export const clearFiltersAndErrors = () => {
const porkemonPageStore = usePokemonListStore()
porkemonPageStore.error = false
porkemonPageStore.filters.criteria = ""
porkemonPageStore.filters.experience = undefined;
porkemonPageStore.filters.moves = undefined;
porkemonPageStore.filters.types = []
}

export const pokemonFromList = (pokemonName: string) => {
const porkemonPageStore = usePokemonListStore()
return porkemonPageStore.pokemons[pokemonName]
}
export const filterByExperience = (pokemons: Pokemon[]):Pokemon[] => {
export const filterByExperience = (pokemons: Pokemon[]): Pokemon[] => {
const porkemonPageStore = usePokemonListStore()
const experience = porkemonPageStore.filters.experience;
if (typeof experience === "undefined") {
Expand Down
Loading

0 comments on commit bc02be6

Please sign in to comment.