diff --git a/frontend/src/components/Map.vue b/frontend/src/components/Map.vue index 9139ebd..416272d 100644 --- a/frontend/src/components/Map.vue +++ b/frontend/src/components/Map.vue @@ -120,13 +120,12 @@ import FloatingButton from '../elements/FloatingButton.vue'; import GPSPoint from '../models/GPSPoint'; import LocationQuery from '../models/LocationQuery'; import LocationQueryMixin from '../mixins/LocationQuery.vue'; -import LocationStats from '../models/LocationStats'; import MapSelectOverlay from './MapSelectOverlay.vue'; import MapView from '../mixins/MapView.vue'; import Paginate from '../mixins/Paginate.vue'; import Points from '../mixins/Points.vue'; import Routes from '../mixins/Routes.vue'; -import StatsRequest from '../models/StatsRequest'; +import StatsMixin from '../mixins/Stats.vue'; import Timeline from './Timeline.vue'; import TimelineMetricsConfiguration from '../models/TimelineMetricsConfiguration'; import URLQueryHandler from '../mixins/URLQueryHandler.vue'; @@ -143,6 +142,7 @@ export default { Paginate, Points, Routes, + StatsMixin, URLQueryHandler, ], @@ -314,22 +314,6 @@ export default { await this.processQueryChange(this.locationQuery, oldQuery) }, - async getCountries(): Promise<Country[]> { - return ( - await this.getStats( - new StatsRequest({ - // @ts-ignore - userId: this.$root.user.id, - groupBy: ['country'], - order: 'desc', - }) - ) - ) - .filter((record: LocationStats) => !!record.key.country) - .map((record: LocationStats) => Country.fromCode(record.key.country)) - .filter((country: Optional<Country>) => !!country) - }, - createMap(): Map { this.pointsLayer = this.createPointsLayer(Object.values(this.mappedPoints) as Point[]) this.routesLayer = this.createRoutesLayer(Object.values(this.mappedPoints) as Point[]) diff --git a/frontend/src/components/filter/Form.vue b/frontend/src/components/filter/Form.vue index fcff5a0..efae992 100644 --- a/frontend/src/components/filter/Form.vue +++ b/frontend/src/components/filter/Form.vue @@ -167,13 +167,10 @@ </p> </label> - <Autocomplete - id="country" - name="country" + <CountrySelector placeholder="Filter by country" - allow-only-values :value="newFilter.country || ''" - :values="autocompleteCountries" + :countries="countries" :disabled="disabled" @input="newFilter.country = $event" /> </div> @@ -274,9 +271,8 @@ <script lang="ts"> import _ from 'lodash' -import Autocomplete from '../../elements/Autocomplete.vue' -import AutocompleteValue from '../../models/AutocompleteValue' import Country from '../../models/Country' +import CountrySelector from '../../elements/CountrySelector.vue' import LocationQuery from '../../models/LocationQuery' import LocationQueryMixin from '../../mixins/LocationQuery.vue' import UserDevice from '../../models/UserDevice' @@ -289,7 +285,7 @@ export default { ], components: { - Autocomplete, + CountrySelector, }, props: { @@ -313,13 +309,6 @@ export default { }, computed: { - autocompleteCountries(): AutocompleteValue[] { - return this.countries.map((country: Country) => ({ - value: country.code, - label: `${country.flag} ${country.name}`, - data: country, - })) - }, maxDate() { return this.toLocalString(this.endPlusHours(new Date(), 0)) } diff --git a/frontend/src/elements/CountrySelector.vue b/frontend/src/elements/CountrySelector.vue new file mode 100644 index 0000000..f7aad57 --- /dev/null +++ b/frontend/src/elements/CountrySelector.vue @@ -0,0 +1,139 @@ +<template> + <div class="country-selector"> + <Loading v-if="loading" /> + <Autocomplete + :id="id" + :name="name" + :placeholder="placeholder" + :value="value" + :values="autocompleteCountries" + :disabled="disabled || loading" + allow-only-values + @input="onInput" /> + </div> +</template> + +<script lang="ts"> +import { countries } from 'countries-list' + +import Autocomplete from './Autocomplete.vue' +import AutocompleteValue from '../models/AutocompleteValue' +import Country from '../models/Country' +import Loading from './Loading.vue' +import Stats from '../mixins/Stats.vue' + +export default { + mixins: [Stats], + components: { + Autocomplete, + Loading, + }, + + props: { + countries: { + type: Array as () => Country[], + }, + + disabled: { + type: Boolean, + default: false, + }, + + id: { + type: String, + default: 'country', + }, + + name: { + type: String, + default: 'country', + }, + + placeholder: { + type: String, + default: 'Select a country', + }, + + showAll: { + type: Boolean, + default: false, + }, + + value: { + type: String, + default: '', + }, + }, + + data() { + return { + countries_: this.countries ? [...this.countries] : [], + loading: false, + } + }, + + computed: { + autocompleteCountries(): AutocompleteValue[] { + const visitedCountries = this.countries_.reduce( + (acc: Record<string, AutocompleteValue>, country: Country) => { + acc[country.code] = this.toAutocompleteValue(country) + return acc + }, {} + ); + + if (!this.showAll) { + return Object.values(visitedCountries) + } + + const unvisitedCountries = Object.keys(countries).reduce( + (acc: Record<string, AutocompleteValue>, key: string) => { + if (visitedCountries[key]) { + return acc + } + acc[key] = this.toAutocompleteValue(countries[key]) + return acc + }, {} + ) + + return [ + ...Object.values(visitedCountries) as AutocompleteValue[], + ...Object.values(unvisitedCountries) as AutocompleteValue[], + ] + }, + }, + + methods: { + onInput(value: string) { + this.$emit('input', value) + }, + + toAutocompleteValue(country: { + code: string + name: string + flag: string + }): AutocompleteValue { + return new AutocompleteValue({ + value: country.code, + label: `${country.flag} ${country.name}`, + data: country, + }) + }, + }, + + created: function () { + if (!this.countries) { + this.getCountries().then((countries: Country[]) => { + this.countries_ = countries + }) + } + }, +} +</script> + +<style scoped lang="scss"> +.country-selector { + position: relative; + width: 100%; + margin: 0 auto; +} +</style> diff --git a/frontend/src/mixins/Stats.vue b/frontend/src/mixins/Stats.vue new file mode 100644 index 0000000..7040151 --- /dev/null +++ b/frontend/src/mixins/Stats.vue @@ -0,0 +1,26 @@ +<script lang="ts"> +import type { Optional } from '../models/Types'; +import Country from '../models/Country'; +import LocationStats from '../models/LocationStats'; +import StatsRequest from '../models/StatsRequest'; + +export default { + methods: { + async getCountries(): Promise<Country[]> { + return ( + await this.getStats( + new StatsRequest({ + // @ts-ignore + userId: this.$root.user.id, + groupBy: ['country'], + order: 'desc', + }) + ) + ) + .filter((record: LocationStats) => !!record.key.country) + .map((record: LocationStats) => Country.fromCode(record.key.country)) + .filter((country: Optional<Country>) => !!country) + }, + }, +} +</script>