Create advocates filter (#1122)

This commit is contained in:
Eddybrando Vásquez 2020-12-11 10:16:10 +01:00 committed by Salvador de la Puente González
parent 9c891b7aaa
commit 5ae48967ac
9 changed files with 253 additions and 58 deletions

View File

@ -91,4 +91,14 @@ $logo-size-small: 8rem;
$icon-size: 0.75rem;
$carbon--theme: $carbon--theme--custom;
@include carbon--theme();
@include carbon--theme();
//== Checkbox
.bx--checkbox:focus + .bx--checkbox-label::before {
box-shadow: 0 0 0 2px $white, 0 0 0 4px $purple-60;
}
.bx--checkbox-label::before {
border: 1px solid $black-100;
}

View File

@ -17,16 +17,24 @@
<template slot="filters-on-m-l-screen">
<AppFieldset :label="filter.label">
<client-only>
<AppCheckbox
<cv-checkbox
v-for="option in filter.options"
:key="option"
:option="option"
:checked="isRegionFilterChecked(option)"
:label="option"
:value="option"
@change="updateRegionFilter(option, $event)"
/>
</client-only>
</AppFieldset>
</template>
<template slot="filters-on-s-screen">
<AppMultiSelect v-bind="filter" />
<AppMultiSelect
:label="filter.label"
:options="filter.options"
:value="regionFilters"
@change-selection="updateRegionFilters($event)"
/>
</template>
<template slot="results">
<InfiniteScroll
@ -44,33 +52,63 @@
<script lang="ts">
import Vue from 'vue'
import { mapState, MapperForStateWithNamespace } from 'vuex'
import { Component, Prop } from 'vue-property-decorator'
import AdvocateCard from '~/components/advocates/AdvocateCard.vue'
import AppMultiSelect from '~/components/ui/AppMultiSelect.vue'
import AppFieldset from '~/components/ui/AppFieldset.vue'
import AppCheckbox from '~/components/ui/AppCheckbox.vue'
import AppFiltersResultsLayout from '~/components/ui/AppFiltersResultsLayout.vue'
import InfiniteScroll from '~/components/ui/InfiniteScroll.vue'
import AppLink from '~/components/ui/AppLink.vue'
import { Advocate, ADVOCATES_WORLD_REGION_OPTIONS, State } from '~/store/modules/advocates.ts'
@Component({
components: {
AdvocateCard,
AppMultiSelect,
AppFieldset,
AppCheckbox,
AppFiltersResultsLayout,
InfiniteScroll,
AppLink
},
computed: {
...mapState<MapperForStateWithNamespace>('advocates', {
regionFilters: (state: State) => state.regionFilters
})
}
})
export default class extends Vue {
@Prop(Array) advocates!: any
export default class MeetTheAdvocates extends Vue {
@Prop(Array) advocates!: Advocate[]
filter = {
/**
* Region filters from Vuex store.
*
* Initialized with mapState.
*/
public regionFilters!: string[]
private filter = {
label: 'Locations',
options: ['Americas', 'Asia Pacific', 'Europe', 'Africa'],
filterType: 'regionFilters'
options: ADVOCATES_WORLD_REGION_OPTIONS
}
isRegionFilterChecked (filterValue: string): boolean {
return this.regionFilters.includes(filterValue)
}
updateRegionFilter (option: string, isChecked: boolean): void {
const regionFilters = this.regionFilters.filter(oldOption => oldOption !== option)
if (isChecked) {
regionFilters.push(option)
}
this.updateRegionFilters(regionFilters)
}
updateRegionFilters (regionFilters: string[]): void {
this.$store.commit('advocates/setRegionFilters', regionFilters)
}
joinSlackLink: string = 'https://ibm.co/joinqiskitslack'

6
package-lock.json generated
View File

@ -2842,6 +2842,12 @@
"integrity": "sha512-Q7DYAOi9O/+cLLhdaSvKdaumWyHbm7HAk/bFwwyTuU0arR5yyCeW5GOoqt4tJTpDRxhpx9Q8kQL6vMpuw9hDSw==",
"dev": true
},
"@types/lodash": {
"version": "4.14.165",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.165.tgz",
"integrity": "sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg==",
"dev": true
},
"@types/markdown-it": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-10.0.3.tgz",

View File

@ -27,6 +27,7 @@
"@nuxtjs/axios": "^5.12.4",
"carbon-components": "^10.25.0",
"cross-env": "^7.0.3",
"lodash": "^4.17.20",
"nuxt": "^2.14.11",
"nuxt-lazy-load": "^1.2.4",
"ts-node": "^9.1.1",
@ -43,6 +44,7 @@
"@nuxtjs/style-resources": "^1.0.0",
"@types/airtable": "^0.8.1",
"@types/jest": "^26.0.19",
"@types/lodash": "^4.14.165",
"@types/markdown-it": "^10.0.3",
"@types/markdown-it-anchor": "^4.0.4",
"@types/markdown-it-link-attributes": "^3.0.0",

View File

@ -7,7 +7,7 @@
</template>
<script lang="ts">
import { mapGetters, mapActions } from 'vuex'
import { mapGetters } from 'vuex'
import { Component } from 'vue-property-decorator'
import QiskitPage from '~/components/logic/QiskitPage.vue'
@ -19,21 +19,13 @@ import QiskitPage from '~/components/logic/QiskitPage.vue'
},
computed: {
...mapGetters([
...mapGetters('advocates', [
'filteredAdvocates'
])
},
methods: {
...mapActions({
fetchAdvocates: 'fetchAdvocates'
})
},
async fetch ({ store }) {
const advocates = await store.dispatch('fetchAdvocates')
store.commit('setAdvocates', advocates)
await store.dispatch('advocates/fetchAdvocates')
}
})
export default class AdvocatesPage extends QiskitPage {

View File

@ -6,6 +6,8 @@ import learningResources from './modules/learning-resources'
Vue.use(Vuex)
export default () => new Vuex.Store({
export const storeOptions = {
modules: { events, advocates, learningResources }
})
}
export default () => new Vuex.Store(storeOptions)

View File

@ -1,3 +1,5 @@
import { ActionTree, GetterTree, MutationTree } from 'vuex'
const ADVOCATES_WORLD_REGIONS = Object.freeze({
northAmerica: 'North America',
southAmerica: 'South America',
@ -9,14 +11,18 @@ const ADVOCATES_WORLD_REGIONS = Object.freeze({
type AdvocatesWorldRegion = typeof ADVOCATES_WORLD_REGIONS[keyof typeof ADVOCATES_WORLD_REGIONS]
type Advocate = {
name: string,
image: string,
city: string,
country: string,
region: AdvocatesWorldRegion,
slackId: string,
slackUsername: string
/**
* Interface for a Qiskit advocate.
*/
interface Advocate {
city: string
country: string
image: string
location?: string
name: string
region: AdvocatesWorldRegion
slackId?: string
slackUsername?: string
}
const ADVOCATES_WORLD_REGION_OPTIONS = Object.freeze([
@ -35,28 +41,48 @@ export {
Advocate
}
export default {
state () {
return {
advocates: []
}
},
getters: {
filteredAdvocates (state: any) {
const { advocates } = state
export class State {
advocates: Advocate[] = []
regionFilters: string[] = []
}
const getters = <GetterTree<State, any>> {
/**
* List of advocates filtered by selected regions.
*/
filteredAdvocates ({ advocates, regionFilters }): Advocate[] {
const noRegionFilters = regionFilters.length === 0
if (noRegionFilters) {
return advocates
}
},
mutations: {
setAdvocates (state: any, payload: any) {
state.advocates = payload
}
},
actions: {
async fetchAdvocates () {
const advocatesModule = await import('~/content/advocates/advocates.json')
return advocatesModule.default || []
}
return advocates.filter(advocate => regionFilters.includes(advocate.region))
}
}
const mutations = <MutationTree<State>> {
setAdvocates (state, advocates: Advocate[]) {
state.advocates = advocates
},
setRegionFilters (state, regionFilters: string[]) {
state.regionFilters = regionFilters
}
}
const actions = <ActionTree<State, any>> {
async fetchAdvocates ({ commit }): Promise<void> {
const advocatesModule = await import('~/content/advocates/advocates.json')
const advocates = advocatesModule.default || []
commit('setAdvocates', advocates)
}
}
export default {
namespaced: true,
state: new State(),
actions,
mutations,
getters
}

View File

@ -43,12 +43,6 @@ type CommunityEvent = {
to: string
}
type EventMultiSelectOption = {
label: string,
value: string,
name: string
}
type EventPayload = {
events: string,
eventsSet: CommunityEvent[]

View File

@ -0,0 +1,125 @@
import Vuex, { Store } from 'vuex'
import cloneDeep from 'lodash/cloneDeep'
import { storeOptions } from '~/store'
let store: Store<any>
const mockAdvocate1 = () => ({
city: 'Lima',
country: 'Peru',
image: 'https://example.com/img/1.jpg',
name: 'John Doe',
region: 'South America'
})
const mockAdvocate2 = () => ({
city: 'Munich',
country: 'Germany',
image: 'https://example.com/img/2.jpg',
name: 'Max Mustermann',
region: 'Europe'
})
/**
* GETTERS
* -----------------------------------------------------------------------------
*/
describe('filteredAdvocates', () => {
const getter = 'advocates/filteredAdvocates'
const mockMatchingRegionFilter1 = () => 'South America'
const mockMatchingRegionFilter2 = () => 'Europe'
const mockNonMatchingRegionFilter = () => 'Moon'
beforeEach(() => {
const initialStoreOptions = cloneDeep(storeOptions)
store = new Vuex.Store(initialStoreOptions)
store.commit('advocates/setAdvocates', [mockAdvocate1(), mockAdvocate2()])
})
it('returns a filtered list of advocates for 1 matching filter', () => {
store.commit('advocates/setRegionFilters', [mockMatchingRegionFilter1()])
expect(store.getters[getter]).toEqual([mockAdvocate1()])
})
it('returns a filtered list of advocates for 2 matching filters', () => {
store.commit('advocates/setRegionFilters', [mockMatchingRegionFilter1(), mockMatchingRegionFilter2()])
expect(store.getters[getter]).toEqual([mockAdvocate1(), mockAdvocate2()])
})
it('returns an empty filtered list of advocates for no matching filters', () => {
store.commit('advocates/setRegionFilters', [mockNonMatchingRegionFilter()])
expect(store.getters[getter]).toEqual([])
})
it('returns the complete list of advocates when there are no filters', () => {
store.commit('advocates/setRegionFilters', [])
expect(store.getters[getter]).toEqual([mockAdvocate1(), mockAdvocate2()])
})
})
/**
* MUTATIONS
* -----------------------------------------------------------------------------
*/
describe('setAdvocates', () => {
const mutationType = 'advocates/setAdvocates'
beforeEach(() => {
const initialStoreOptions = cloneDeep(storeOptions)
store = new Vuex.Store(initialStoreOptions)
})
it('sets the list of advocates with one advocate', () => {
store.commit(mutationType, [mockAdvocate1()])
expect(store.state.advocates.advocates).toEqual([mockAdvocate1()])
})
it('sets the list of advocates twice and keeps only the latest list', () => {
store.commit(mutationType, [mockAdvocate1()])
store.commit(mutationType, [mockAdvocate2()])
expect(store.state.advocates.advocates).toEqual([mockAdvocate2()])
})
it('sets the list of advocates with multiple advocates', () => {
store.commit(mutationType, [mockAdvocate1(), mockAdvocate2()])
expect(store.state.advocates.advocates).toEqual([mockAdvocate1(), mockAdvocate2()])
})
it('unsets the list of advocates', () => {
store.commit(mutationType, [])
expect(store.state.advocates.advocates).toEqual([])
})
})
describe('setRegionFilters', () => {
const mutationType = 'advocates/setRegionFilters'
const mockRegionFilter1 = 'South America'
const mockRegionFilter2 = 'Europe'
beforeEach(() => {
const initialStoreOptions = cloneDeep(storeOptions)
store = new Vuex.Store(initialStoreOptions)
})
it('sets the region filters with one filter', () => {
store.commit(mutationType, [mockRegionFilter1])
expect(store.state.advocates.regionFilters).toEqual([mockRegionFilter1])
})
it('sets the region filters twice and keeps only the latest filters', () => {
store.commit(mutationType, [mockRegionFilter1])
store.commit(mutationType, [mockRegionFilter2])
expect(store.state.advocates.regionFilters).toEqual([mockRegionFilter2])
})
it('sets the region filters with multiple filters', () => {
store.commit(mutationType, [mockRegionFilter1, mockRegionFilter2])
expect(store.state.advocates.regionFilters).toEqual([mockRegionFilter1, mockRegionFilter2])
})
it('unsets the region filters', () => {
store.commit(mutationType, [])
expect(store.state.advocates.regionFilters).toEqual([])
})
})