kopia lustrzana https://github.com/manuelkasper/sotlas-frontend
464 wiersze
15 KiB
Vue
464 wiersze
15 KiB
Vue
<template>
|
|
<PageLayout>
|
|
<template v-slot:title><CountryFlag v-if="country" :country="country" class="flag" />{{ callsign }}</template>
|
|
<template v-slot:title-right><CallDatabaseButton :callsign="callsign" /></template>
|
|
<template v-slot:subtitle>
|
|
<div v-if="activator" class="subtitle is-size-7-mobile">
|
|
<div class="activator-info">
|
|
<span><strong>{{ activator.points + activator.bonusPoints }} points</strong><template v-if="activator.bonusPoints > 0"> ({{ activator.bonusPoints }} bonus)</template></span>
|
|
<span v-if="mountainGoats > 0"><b-tooltip class="goat-tooltip" label="mountain goat"><svgicon class="goat-icon" icon="goat" /></b-tooltip> x {{ mountainGoats }}</span>
|
|
</div>
|
|
<div class="activator-info">
|
|
<span><font-awesome-icon :icon="['far', 'chevron-circle-up']" class="faicon" /> {{ activator.summits }} activations<template v-if="activationsThisYear"> ({{ activationsThisYear }} this year)</template></span>
|
|
<span v-if="uniqueSummits"><font-awesome-icon :icon="['far', 'mountains']" class="faicon" /> {{ uniqueSummits }} unique ({{ uniqueSummitsThisYear }} this year)</span>
|
|
<span v-if="numQsos"><font-awesome-icon :icon="['far', 'exchange']" class="faicon" /> {{ numQsos }} QSOs</span>
|
|
<span v-if="associationCount"><font-awesome-icon :icon="['far', 'globe']" class="faicon" /> {{ associationCount }} associations</span>
|
|
<span v-if="activatorSince"><font-awesome-icon :icon="['far', 'history']" class="faicon" /> {{ activatorSince }}</span>
|
|
<LoadingSpinner v-if="activationsLoading" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template>
|
|
<section v-if="recentSpots.length > 0 || $store.state.spots.length === 0" class="section">
|
|
<div class="container">
|
|
|
|
<div class="level is-mobile">
|
|
<div class="level-left">
|
|
<h4 class="title is-4">Recent SOTA spots</h4>
|
|
</div>
|
|
<div class="level-right">
|
|
<LiveFeedIndicator />
|
|
</div>
|
|
</div>
|
|
|
|
<SpotsList v-if="recentSpots.length > 0" class="auto-width" :data="recentSpots" :callsignLink="false" :paginated="recentSpots.length > 10" />
|
|
<p v-else-if="$store.state.spots.length === 0"><b-loading :active="true" :is-full-page="false" />Loading...</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section v-if="(rbnSpots !== null && rbnSpots.length > 0) || rbnSpots === null" class="section">
|
|
<div class="container">
|
|
<div class="level is-mobile">
|
|
<div class="level-left">
|
|
<h4 class="title is-4">Recent RBN spots</h4>
|
|
</div>
|
|
<div class="level-right">
|
|
<LiveFeedIndicator />
|
|
</div>
|
|
</div>
|
|
|
|
<RBNSpotsList v-if="rbnSpots !== null && rbnSpots.length > 0" class="auto-width" :data="rbnSpots" :callsignLink="false" :paginated="rbnSpots.length > 10" />
|
|
<p v-else-if="rbnSpots === null"><b-loading :active="true" :is-full-page="false" />Loading...</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section v-if="alerts.length > 0" class="section">
|
|
<div class="container">
|
|
<h4 class="title is-4">Alerts</h4>
|
|
|
|
<AlertsList v-if="alerts.length > 0" class="auto-width" :data="alerts" :callsignLink="false" :paginated="alerts.length > 10" />
|
|
</div>
|
|
</section>
|
|
|
|
<hr v-if="(recentSpots.length > 0 || $store.state.spots.length === 0) || ((rbnSpots !== null && rbnSpots.length > 0) || rbnSpots === null) || alerts.length > 0" />
|
|
|
|
<template v-if="activations.length > 0 || activationsLoading">
|
|
<section class="section">
|
|
<div class="container">
|
|
<ActivationCharts v-if="activations.length > 0" :activations="activations" />
|
|
<p class="loading-charts" v-else-if="activationsLoading"><b-loading :active="true" :is-full-page="false" /><font-awesome-icon :icon="['far', 'chart-bar']" /></p>
|
|
</div>
|
|
</section>
|
|
|
|
<hr />
|
|
</template>
|
|
|
|
<section class="section">
|
|
<div class="container">
|
|
<h4 class="title is-4 logged-act"><span>Logged activations</span><b-button v-if="!notFound" size="is-small" icon-left="map" icon-pack="fas" type="is-info" @click="showMap = !showMap">{{ showMap ? 'Hide' : 'Show' }} Map</b-button></h4>
|
|
|
|
<template v-if="activations !== null && activations.length > 0">
|
|
<MiniMap v-if="showMap" class="map" :bounds="activationsMapBounds" :filter="activationsMapFilter" zoom-warning show-inactive-summits />
|
|
|
|
<b-field>
|
|
<FilterInput v-model="activationsFilter" :is-regex="true" />
|
|
</b-field>
|
|
|
|
<ActivationsList v-if="activations !== null && activations.length > 0" :data="filteredActivations" :infinite="true" :ownCallsign="callsign" />
|
|
</template>
|
|
|
|
<b-message v-if="databaseError" type="is-warning" has-icon>
|
|
SOTA database error, try again later.
|
|
</b-message>
|
|
<b-message v-else-if="notFound" type="is-info" has-icon>
|
|
Activator not found in database. Note: new activators (or callsign changes) may take up to 24 hours to propagate to SOTLAS.
|
|
</b-message>
|
|
<b-message v-else-if="!activationsLoading && activations.length === 0" type="is-info" has-icon>
|
|
No activations found.
|
|
</b-message>
|
|
|
|
<p v-if="activationsLoading"><b-loading :active="true" :is-full-page="false" />Loading...</p>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
</PageLayout>
|
|
</template>
|
|
|
|
<script>
|
|
import axios from 'axios'
|
|
import moment from 'moment'
|
|
import utils from '../mixins/utils.js'
|
|
import api from '../mixins/api.js'
|
|
import prefix from '../prefix.js'
|
|
import EventBus from '../event-bus'
|
|
import PageLayout from '../components/PageLayout.vue'
|
|
import ActivationsList from '../components/ActivationsList.vue'
|
|
import SpotsList from '../components/SpotsList.vue'
|
|
import RBNSpotsList from '../components/RBNSpotsList.vue'
|
|
import AlertsList from '../components/AlertsList.vue'
|
|
import LiveFeedIndicator from '../components/LiveFeedIndicator.vue'
|
|
import FilterInput from '../components/FilterInput.vue'
|
|
import MiniMap from '../components/MiniMap.vue'
|
|
import ActivationCharts from '../components/ActivationCharts.vue'
|
|
import LoadingSpinner from '../components/LoadingSpinner.vue'
|
|
import CountryFlag from '../components/CountryFlag.vue'
|
|
import CallDatabaseButton from '../components/CallDatabaseButton.vue'
|
|
|
|
export default {
|
|
name: 'Activator',
|
|
props: {
|
|
callsign: String
|
|
},
|
|
delayScroll: true,
|
|
components: {
|
|
PageLayout, ActivationsList, SpotsList, RBNSpotsList, AlertsList, LiveFeedIndicator, FilterInput, MiniMap, ActivationCharts, LoadingSpinner, CountryFlag, CallDatabaseButton
|
|
},
|
|
mixins: [utils, api],
|
|
computed: {
|
|
mountainGoats () {
|
|
if (!this.activator) {
|
|
return 0
|
|
}
|
|
return Math.floor((this.activator.points + this.activator.bonusPoints) / 1000)
|
|
},
|
|
numQsos () {
|
|
if (this.activations.length === 0) {
|
|
return null
|
|
}
|
|
|
|
let qsos = 0
|
|
this.activations.forEach(activation => {
|
|
qsos += activation.qsos
|
|
})
|
|
return qsos
|
|
},
|
|
recentSpots () {
|
|
let myCallsign = this.homeCallsign(this.callsign)
|
|
return this.$store.state.spots.filter(spot => {
|
|
return (this.homeCallsign(spot.activatorCallsign) === myCallsign)
|
|
})
|
|
},
|
|
filteredActivations () {
|
|
if (this.filter === '') {
|
|
return this.activations
|
|
}
|
|
try {
|
|
let filterRegex = new RegExp(this.activationsFilter, 'i')
|
|
return this.activations.filter(activation => {
|
|
return filterRegex.test(activation.summit.code) || filterRegex.test(activation.summit.name)
|
|
})
|
|
} catch (e) {
|
|
return []
|
|
}
|
|
},
|
|
alerts () {
|
|
let myCallsign = this.homeCallsign(this.callsign)
|
|
return this.$store.state.alerts.filter(alert => {
|
|
return (this.homeCallsign(alert.activatorCallsign) === myCallsign)
|
|
})
|
|
},
|
|
activatorSince () {
|
|
if (this.activations.length === 0) {
|
|
return null
|
|
}
|
|
|
|
let firstActivationDate = this.activations.map(activation => activation.date).reduce((acc, date) => {
|
|
if (date < acc) {
|
|
return date
|
|
}
|
|
return acc
|
|
})
|
|
let now = moment.utc()
|
|
let then = moment.utc(firstActivationDate)
|
|
let months = now.diff(then, 'months')
|
|
|
|
if (months < 12) {
|
|
return months + ' months'
|
|
} else {
|
|
let years = Math.floor(months / 12)
|
|
months %= 12
|
|
let since = years + (years > 1 ? ' years' : ' year')
|
|
if (months > 0) {
|
|
since += ', ' + months + (months > 1 ? ' months' : ' month')
|
|
}
|
|
return since
|
|
}
|
|
},
|
|
activationsThisYear () {
|
|
if (this.activations.length === 0) {
|
|
return null
|
|
}
|
|
|
|
let now = moment.utc()
|
|
let activations = 0
|
|
this.activations.forEach(activation => {
|
|
if (!now.isSame(activation.date, 'year')) {
|
|
return
|
|
}
|
|
|
|
activations++
|
|
})
|
|
return activations
|
|
},
|
|
uniqueSummits () {
|
|
if (this.activations.length === 0) {
|
|
return null
|
|
}
|
|
|
|
let summits = new Set()
|
|
this.activations.forEach(activation => {
|
|
summits.add(activation.summit.code)
|
|
})
|
|
|
|
return summits.size
|
|
},
|
|
uniqueSummitsThisYear () {
|
|
if (this.activations.length === 0) {
|
|
return null
|
|
}
|
|
|
|
let summits = new Set()
|
|
let summitsPreviousYears = new Set()
|
|
let now = moment.utc()
|
|
this.activations.forEach(activation => {
|
|
summits.add(activation.summit.code)
|
|
if (!now.isSame(activation.date, 'year')) {
|
|
summitsPreviousYears.add(activation.summit.code)
|
|
}
|
|
})
|
|
|
|
return summits.size - summitsPreviousYears.size
|
|
},
|
|
country () {
|
|
return prefix.isoCodeForCallsign(this.callsign)
|
|
},
|
|
associationCount () {
|
|
if (this.activations.length === 0) {
|
|
return null
|
|
}
|
|
|
|
let associations = new Set()
|
|
this.activations.forEach(activation => {
|
|
associations.add(activation.summit.code.substring(0, activation.summit.code.indexOf('/')))
|
|
})
|
|
return associations.size
|
|
},
|
|
activationsMapBounds () {
|
|
let minLat, minLon, maxLat, maxLon
|
|
this.activations.forEach(activation => {
|
|
if (!minLat || activation.summit.coordinates.latitude < minLat) {
|
|
minLat = activation.summit.coordinates.latitude
|
|
}
|
|
if (!maxLat || activation.summit.coordinates.latitude > maxLat) {
|
|
maxLat = activation.summit.coordinates.latitude
|
|
}
|
|
if (!minLon || activation.summit.coordinates.longitude < minLon) {
|
|
minLon = activation.summit.coordinates.longitude
|
|
}
|
|
if (!maxLon || activation.summit.coordinates.longitude > maxLon) {
|
|
maxLon = activation.summit.coordinates.longitude
|
|
}
|
|
})
|
|
|
|
// Some padding
|
|
let latDiff = maxLat - minLat
|
|
let lonDiff = maxLon - minLon
|
|
minLat -= (latDiff * 0.05)
|
|
maxLat += (latDiff * 0.05)
|
|
minLon -= (lonDiff * 0.05)
|
|
maxLon += (lonDiff * 0.05)
|
|
|
|
minLat = Math.max(Math.min(minLat, 90), -90)
|
|
maxLat = Math.max(Math.min(maxLat, 90), -90)
|
|
minLon = Math.max(Math.min(minLon, 180), -180)
|
|
maxLon = Math.max(Math.min(maxLon, 180), -180)
|
|
|
|
return [[minLon, minLat], [maxLon, maxLat]]
|
|
},
|
|
activationsMapFilter () {
|
|
let summits = new Set()
|
|
this.activations.forEach(activation => {
|
|
summits.add(activation.summit.code)
|
|
})
|
|
return ['in', 'code', ...summits]
|
|
}
|
|
},
|
|
watch: {
|
|
callsign () {
|
|
this.updateCallsign()
|
|
}
|
|
},
|
|
methods: {
|
|
receiveRbnSpot (rbnSpot) {
|
|
if (rbnSpot.homeCallsign === this.callsign) {
|
|
if (this.rbnSpots === null) {
|
|
this.rbnSpots = []
|
|
}
|
|
if (!this.rbnSpots.find(oldSpot => { return oldSpot._id === rbnSpot._id })) {
|
|
this.rbnSpots.push(rbnSpot)
|
|
}
|
|
}
|
|
},
|
|
receiveRbnSpotHistory (rbnSpots, viewId) {
|
|
if (viewId === this.viewId) {
|
|
this.rbnSpots = rbnSpots
|
|
}
|
|
},
|
|
updateCallsign () {
|
|
if (this.homeCallsign(this.callsign, false) !== this.callsign) {
|
|
this.$router.replace('/activators/' + this.homeCallsign(this.callsign, false))
|
|
return
|
|
}
|
|
|
|
document.title = this.callsign + ' - SOTLAS'
|
|
this.activationsLoading = true
|
|
this.activations = []
|
|
this.rbnSpots = null
|
|
this.activator = null
|
|
this.notFound = false
|
|
this.databaseError = false
|
|
|
|
let loads = []
|
|
axios.get(process.env.VUE_APP_API_URL + '/activators/' + this.callsign)
|
|
.then(response => {
|
|
if (response) {
|
|
this.activator = response.data
|
|
if (this.activator && this.activator.callsign !== this.callsign) {
|
|
this.$router.replace('/activators/' + this.activator.callsign)
|
|
return
|
|
}
|
|
|
|
this.loadActivations(this.callsign)
|
|
.then(activations => {
|
|
this.activations = activations
|
|
this.activationsLoading = false
|
|
|
|
if (this.activations && this.activations.length) {
|
|
// Recalculate points
|
|
let points = 0
|
|
let bonusPoints = 0
|
|
this.activations.forEach(activation => {
|
|
points += activation.points
|
|
bonusPoints += activation.bonus
|
|
})
|
|
this.activator.points = points
|
|
this.activator.bonusPoints = bonusPoints
|
|
this.activator.summits = this.activations.length
|
|
}
|
|
})
|
|
.catch(() => {
|
|
this.databaseError = true
|
|
this.activationsLoading = false
|
|
})
|
|
}
|
|
})
|
|
.catch(error => {
|
|
this.activationsLoading = false
|
|
if (error.response && error.response.status === 404) {
|
|
this.notFound = true
|
|
}
|
|
})
|
|
|
|
Promise.all(loads)
|
|
.then(() => {
|
|
this.$root.$emit('triggerScroll')
|
|
})
|
|
|
|
this.$store.commit('setRbnFilter', { homeCallsign: this.homeCallsign(this.callsign), maxAge: 86400000, viewId: this.viewId })
|
|
}
|
|
},
|
|
mounted () {
|
|
this.updateCallsign()
|
|
EventBus.$on('rbnSpot', this.receiveRbnSpot)
|
|
EventBus.$on('rbnSpotHistory', this.receiveRbnSpotHistory)
|
|
},
|
|
destroyed () {
|
|
this.$store.commit('setRbnFilter', {})
|
|
EventBus.$off('rbnSpot', this.receiveRbnSpot)
|
|
EventBus.$off('rbnSpotHistory', this.receiveRbnSpotHistory)
|
|
},
|
|
data () {
|
|
return {
|
|
activations: [],
|
|
notFound: false,
|
|
databaseError: false,
|
|
rbnSpots: null,
|
|
activationsLoading: true,
|
|
activationsFilter: '',
|
|
activator: null,
|
|
viewId: new Date().getTime(),
|
|
showMap: false
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.b-table >>> .level {
|
|
padding-bottom: 0;
|
|
}
|
|
.activator-info > span {
|
|
display: inline-block;
|
|
}
|
|
.activator-info > span::after {
|
|
content: '|';
|
|
color: #ccc;
|
|
padding: 0 0.4em;
|
|
}
|
|
.activator-info > span:last-child::after {
|
|
content: '';
|
|
}
|
|
.activator-info .faicon {
|
|
margin-right: 0.1em;
|
|
opacity: 0.5;
|
|
}
|
|
.logged-act span {
|
|
margin-right: 0.75em;
|
|
}
|
|
.loading-charts {
|
|
text-align: center;
|
|
}
|
|
.loading-charts .fa-chart-bar {
|
|
font-size: 6rem;
|
|
color: #ddd;
|
|
}
|
|
.goat-tooltip {
|
|
vertical-align: -0.25em;
|
|
}
|
|
.goat-icon {
|
|
width: 1.25em;
|
|
height: 1.25em;
|
|
opacity: 0.35;
|
|
}
|
|
.title .flag {
|
|
margin-right: 1.1rem;
|
|
}
|
|
.map {
|
|
width: 100%;
|
|
height: 70vh;
|
|
border: 1px solid #ccc;
|
|
margin-bottom: 2em;
|
|
}
|
|
</style>
|