kopia lustrzana https://github.com/manuelkasper/sotlas-frontend
Don't auto-select first autocomplete entry, to avoid timing issues with pressing enter key before results have been loaded. Reintroduce separate search results page and add places search there too. Fixes #31
rodzic
20e433b89c
commit
d797895b23
|
@ -8,7 +8,7 @@
|
|||
:loading="isLoading"
|
||||
:open-on-focus="true"
|
||||
:clear-on-select="true"
|
||||
:keep-first="true"
|
||||
:keep-first="false"
|
||||
placeholder="Summit, Callsign, Coords, Place..."
|
||||
field="label"
|
||||
icon-pack="far"
|
||||
|
@ -19,6 +19,7 @@
|
|||
@select="onSelect"
|
||||
@focus="searchFocus"
|
||||
@blur="searchBlur"
|
||||
@keydown.native.enter="doSearch"
|
||||
>
|
||||
<template slot="empty">
|
||||
<span v-if="isLoading">Searching...</span>
|
||||
|
@ -88,6 +89,12 @@ const PLACE_TYPE_ICONS = {
|
|||
poi: 'landmark'
|
||||
}
|
||||
|
||||
const COORDINATE_REGEX = /^\s*(-?[0-9.]+)\s*,\s*(-?[0-9.]+)\s*$/
|
||||
const REGION_REGEX = /^[A-Z0-9]{1,3}\/[A-Z]{2}$/i
|
||||
const SUMMIT_REF_EXACT_REGEX = /^([A-Z0-9]{1,3})\/([A-Z]{2})-([0-9]{3})$/i
|
||||
const SUMMIT_REF_RELAXED_REGEX = /^([A-Z0-9]{1,3})[/ ]?([A-Z]{2})[- ]?([0-9]{3})$/i
|
||||
const REGION_NUM_REGEX = /^([A-Z]{2})[ -]?([0-9]{3})$/i
|
||||
|
||||
maptilersdk.config.apiKey = process.env.VUE_APP_MAPTILER_KEY
|
||||
|
||||
export default {
|
||||
|
@ -137,7 +144,7 @@ export default {
|
|||
}
|
||||
},
|
||||
makeCoordinateResult (value) {
|
||||
const coordMatches = value.match(/^\s*(-?[0-9.]+)\s*,\s*(-?[0-9.]+)\s*$/)
|
||||
const coordMatches = value.match(COORDINATE_REGEX)
|
||||
if (coordMatches) {
|
||||
return {
|
||||
type: 'coordinates',
|
||||
|
@ -148,7 +155,7 @@ export default {
|
|||
return null
|
||||
},
|
||||
makeRegionResult (value) {
|
||||
const regionMatches = value.match(/^[A-Z0-9]{1,3}\/[A-Z]{2}$/i)
|
||||
const regionMatches = value.match(REGION_REGEX)
|
||||
if (regionMatches) {
|
||||
return {
|
||||
type: 'region',
|
||||
|
@ -216,7 +223,7 @@ export default {
|
|||
// Accepts e.g. HBVS123, HB/VS-123, HB VS 123, etc.
|
||||
// Converts to HB/VS-123
|
||||
let ref = value.trim().toUpperCase()
|
||||
let m = ref.match(/^([A-Z0-9]{1,8})[/ ]?([A-Z]{2})[- ]?([0-9]{3})$/)
|
||||
let m = ref.match(SUMMIT_REF_RELAXED_REGEX)
|
||||
if (m) {
|
||||
return `${m[1]}/${m[2]}-${m[3]}`
|
||||
}
|
||||
|
@ -298,6 +305,41 @@ export default {
|
|||
}
|
||||
this.$emit('search')
|
||||
},
|
||||
doSearch () {
|
||||
let targetUrl = null
|
||||
if (this.myQuery.length > 0) {
|
||||
// Coordinates?
|
||||
let coordMatches = this.myQuery.match(COORDINATE_REGEX)
|
||||
if (coordMatches) {
|
||||
targetUrl = '/map/coordinates/' + coordMatches[1] + ',' + coordMatches[2] + '/16.0?popup=1'
|
||||
} else {
|
||||
// Full summit reference?
|
||||
let normalizedRef = this.normalizeSummitRef(this.myQuery)
|
||||
if (SUMMIT_REF_EXACT_REGEX.test(normalizedRef)) {
|
||||
targetUrl = '/summits/' + normalizedRef
|
||||
} else {
|
||||
// Region?
|
||||
let regionMatches = this.myQuery.match(REGION_REGEX)
|
||||
if (regionMatches) {
|
||||
targetUrl = '/summits/' + this.myQuery.toUpperCase()
|
||||
} else {
|
||||
// Region + number without dash (and without association?)
|
||||
let regionNumMatches = this.myQuery.match(REGION_NUM_REGEX)
|
||||
if (regionNumMatches) {
|
||||
this.myQuery = regionNumMatches[1].toUpperCase() + '-' + regionNumMatches[2]
|
||||
}
|
||||
targetUrl = '/search?q=' + encodeURIComponent(this.myQuery)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (targetUrl) {
|
||||
this.$refs.query.isActive = false
|
||||
this.myQuery = ''
|
||||
this.$router.push(targetUrl)
|
||||
}
|
||||
}
|
||||
this.$emit('search')
|
||||
},
|
||||
iconForPlaceType (type) {
|
||||
return PLACE_TYPE_ICONS[type] || 'map-marker-alt'
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import RBNSpots from './views/RBNSpots.vue'
|
|||
import Alerts from './views/Alerts.vue'
|
||||
import NewPhotos from './views/NewPhotos.vue'
|
||||
import SolarHistory from './views/SolarHistory.vue'
|
||||
import SearchAnything from './views/SearchAnything.vue'
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
|
@ -167,6 +168,11 @@ let router = new Router({
|
|||
path: '/solar_history',
|
||||
component: SolarHistory
|
||||
},
|
||||
{
|
||||
path: '/search',
|
||||
component: SearchAnything,
|
||||
meta: { savePath: null }
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
component: NotFound,
|
||||
|
|
|
@ -0,0 +1,227 @@
|
|||
<template>
|
||||
<PageLayout>
|
||||
<template v-slot:title>Search results</template>
|
||||
<template>
|
||||
<section v-if="summits !== null && summits.length > 0" class="section">
|
||||
<div class="container">
|
||||
<h4 class="title is-4"><b-icon icon="mountains" />Summits</h4>
|
||||
<b-field v-if="inactiveCount > 0" grouped>
|
||||
<b-switch v-model="showInactive">Show inactive ({{ inactiveCount }})</b-switch>
|
||||
</b-field>
|
||||
|
||||
<SummitList :data="filteredSummits" auto-width />
|
||||
|
||||
<b-message v-if="summits !== null && summits.length === this.limit" type="is-warning" has-icon>
|
||||
More than {{ this.limit }} summits found, so not all summits may be shown. Please make your search input more specific.
|
||||
</b-message>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="activators !== null && activators.length > 0" class="section">
|
||||
<div class="container">
|
||||
<h4 class="title is-4"><b-icon icon="user" />Activators</h4>
|
||||
|
||||
<b-table class="auto-width" default-sort="callsign" :narrowed="true" :striped="true" :data="activators" :mobile-cards="false">
|
||||
<template slot-scope="props">
|
||||
<b-table-column field="callsign" label="Callsign" sortable>
|
||||
<router-link :to="makeActivatorLink(props.row.callsign)">{{ props.row.callsign }}</router-link>
|
||||
</b-table-column>
|
||||
<b-table-column field="summits" label="Summits" numeric sortable>
|
||||
{{ props.row.summits }}
|
||||
</b-table-column>
|
||||
<b-table-column field="points" :label="$mq.mobile ? 'Pts.' : 'Points'" numeric sortable>
|
||||
{{ props.row.points }}
|
||||
</b-table-column>
|
||||
<b-table-column field="bonusPoints" :label="$mq.mobile ? 'Bonus' : 'Bonus points'" numeric sortable>
|
||||
{{ props.row.bonusPoints }}
|
||||
</b-table-column>
|
||||
<b-table-column field="score" label="Score" numeric sortable>
|
||||
{{ props.row.score }}
|
||||
</b-table-column>
|
||||
<b-table-column v-if="!$mq.mobile" field="avgPoints" label="Avg. points" numeric sortable>
|
||||
{{ props.row.avgPoints }}
|
||||
</b-table-column>
|
||||
</template>
|
||||
</b-table>
|
||||
|
||||
<b-message v-if="activators !== null && activators.length === this.limit" type="is-warning" has-icon>
|
||||
More than {{ this.limit }} activators found, so not all activators may be shown. Please make your search input more specific.
|
||||
</b-message>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="places !== null && places.length > 0" class="section">
|
||||
<div class="container">
|
||||
<h4 class="title is-4"><b-icon icon="map-marker-alt" pack="fas" />Places</h4>
|
||||
|
||||
<b-table class="auto-width" :narrowed="true" :striped="true" :data="places" :mobile-cards="false">
|
||||
<template slot-scope="props">
|
||||
<b-table-column field="label" label="Name">
|
||||
<a @click="goToPlace(props.row)">{{ props.row.label }}</a>
|
||||
</b-table-column>
|
||||
<b-table-column field="detail" label="Detail">
|
||||
{{ props.row.detail }}
|
||||
</b-table-column>
|
||||
</template>
|
||||
</b-table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<b-message v-if="activators !== null && activators.length === 0 && summits !== null && summits.length === 0 && places.length === 0" type="is-info" has-icon>
|
||||
No matching summits, activators, or places for '{{ $route.query.q }}' found.
|
||||
</b-message>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import moment from 'moment'
|
||||
import utils from '../mixins/utils.js'
|
||||
import * as maptilersdk from '@maptiler/sdk'
|
||||
|
||||
import PageLayout from '../components/PageLayout.vue'
|
||||
import SummitList from '../components/SummitList.vue'
|
||||
|
||||
export default {
|
||||
name: 'SearchAnything',
|
||||
components: { PageLayout, SummitList },
|
||||
mixins: [utils],
|
||||
methods: {
|
||||
doSearch () {
|
||||
let loads = []
|
||||
let q = this.$route.query.q.trim()
|
||||
this.loadingComponent = this.$buefy.loading.open({ canCancel: true })
|
||||
loads.push(axios.get(process.env.VUE_APP_API_URL + '/activators/search', { params: { q, limit: this.limit } })
|
||||
.then(response => {
|
||||
this.activators = response.data.activators
|
||||
}))
|
||||
|
||||
loads.push(axios.get(process.env.VUE_APP_API_URL + '/summits/search', { params: { q, limit: this.limit } })
|
||||
.then(response => {
|
||||
let now = moment()
|
||||
response.data.forEach(summit => {
|
||||
summit.isValid = (moment(summit.validFrom).isBefore(now) && moment(summit.validTo).isAfter(now))
|
||||
})
|
||||
this.summits = response.data
|
||||
}))
|
||||
|
||||
// Places search (MapTiler geocoding)
|
||||
let proximity = null
|
||||
if (this.$store.state.mapCenter) {
|
||||
proximity = [this.$store.state.mapCenter.longitude, this.$store.state.mapCenter.latitude]
|
||||
}
|
||||
let geoOpts = {
|
||||
limit: 10,
|
||||
language: 'en',
|
||||
proximity
|
||||
}
|
||||
maptilersdk.config.apiKey = process.env.VUE_APP_MAPTILER_KEY
|
||||
loads.push(
|
||||
maptilersdk.geocoding.forward(q, geoOpts)
|
||||
.then(geoResp => {
|
||||
this.places = (geoResp.features || []).map(f => ({
|
||||
label: f.text,
|
||||
detail: f.place_name.replace(f.text + ', ', ''),
|
||||
coordinates: f.geometry.coordinates
|
||||
}))
|
||||
})
|
||||
.catch(() => { this.places = [] })
|
||||
)
|
||||
|
||||
Promise.all(loads)
|
||||
.then(() => {
|
||||
this.loadingComponent.close()
|
||||
|
||||
if (this.activators.length === 1 && this.summits.length === 0 && this.places.length === 0) {
|
||||
this.$router.replace('/activators/' + this.activators[0].callsign)
|
||||
} else if (this.summits.length === 1 && this.activators.length === 0 && this.places.length === 0) {
|
||||
this.$router.replace('/summits/' + this.summits[0].code)
|
||||
}
|
||||
})
|
||||
},
|
||||
goToPlace (place) {
|
||||
if (place && place.coordinates) {
|
||||
// MapTiler returns [lon, lat]
|
||||
this.$router.push(`/map/coordinates/${place.coordinates[1]},${place.coordinates[0]}/14.0?popup=1`)
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route' (to, from) {
|
||||
this.doSearch()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredSummits () {
|
||||
return this.summits.filter(summit => {
|
||||
return (summit.isValid || this.showInactive)
|
||||
})
|
||||
},
|
||||
inactiveCount () {
|
||||
if (this.summits === null) {
|
||||
return 0
|
||||
}
|
||||
let inactiveCount = 0
|
||||
this.summits.forEach(summit => {
|
||||
if (!summit.isValid) {
|
||||
inactiveCount++
|
||||
}
|
||||
})
|
||||
return inactiveCount
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
document.title = 'Search results - SOTLAS'
|
||||
|
||||
this.doSearch()
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
activators: null,
|
||||
limit: 100,
|
||||
summits: null,
|
||||
showInactive: false,
|
||||
places: []
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.summits >>> .is-invalid {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.points {
|
||||
padding: 0.2em;
|
||||
min-width: 2em;
|
||||
}
|
||||
}
|
||||
@media (max-width: 414px) {
|
||||
.table .summit-name {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 10em;
|
||||
}
|
||||
}
|
||||
@media (max-width: 414px) {
|
||||
.table td, .table th {
|
||||
padding: 0.25em 0.3em;
|
||||
}
|
||||
.table .summit-name {
|
||||
max-width: 8em;
|
||||
}
|
||||
}
|
||||
.message.is-warning {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.title.is-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
Ładowanie…
Reference in New Issue