Timo Tomasini 2022-12-17 18:45:45 +01:00 zatwierdzone przez GitHub
commit 4b0f4d10bd
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
11 zmienionych plików z 385 dodań i 14 usunięć

Wyświetl plik

@ -24,5 +24,5 @@ $link: $blue;
@import "~buefy/src/scss/buefy";
@import "~flagpack/dist/flagpack.css";
@import '~mapbox-gl/dist/mapbox-gl.css';
@import '~@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'
@import '~@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
</style>

Wyświetl plik

@ -112,6 +112,10 @@ export default {
target: '/activators',
text: 'Activators'
},
{
target: '/user_data',
text: 'User Data'
},
{
target: '/settings',
text: 'Settings',

Wyświetl plik

@ -1,5 +1,5 @@
<template>
<b-table :class="{ 'auto-width': autoWidth, summits: true }" default-sort="code" :narrowed="true" :striped="true" :data="data" :mobile-cards="false" :row-class="(row, index) => !row.isValid && 'is-invalid'">
<b-table :class="{ 'auto-width': autoWidth, summits: true }" default-sort="code" :narrowed="true" :striped="true" :data="data" :mobile-cards="false" :row-class="(row, index) => !row.isValid && !ignoreValidity ? 'is-invalid' : ''">
<template slot-scope="props">
<b-table-column field="code" label="Code" class="nowrap" sortable>
<router-link :to="makeSummitLink(props.row.code)">{{ props.row.code }}</router-link>
@ -44,7 +44,8 @@ export default {
myActivatedSummits: Set,
myActivatedSummitsThisYear: Set,
myChasedSummits: Set,
autoWidth: Boolean
autoWidth: Boolean,
ignoreValidity: Boolean
},
mixins: [utils],
components: {

Wyświetl plik

@ -13,7 +13,7 @@ import { faCheck, faCheckCircle, faInfoCircle, faExclamationTriangle, faExclamat
faQuoteRight, faSearch, faMountains, faUser, faClock, faChevronCircleUp, faChevronCircleDown, faChartBar, faFileDownload,
faExchange, faGlobe, faCalendarDay, faTrashAlt, faEdit, faClone, faCheckCircle as farCheckCircle, faArrowsH, faArrowsAlt,
faSnowflake, faWindowMinimize, faWindowMaximize, faWindowClose, faExpandArrows, faLocation, faCalendarCheck, faComment, faSpinner,
faBookUser } from '@fortawesome/pro-regular-svg-icons'
faBookUser, faBookmark, faTag } from '@fortawesome/pro-regular-svg-icons'
import { faMap, faCheckCircle as fasCheckCircle, faChevronCircleDown as fasChevronCircleDown, faChevronCircleUp as fasChevronCircleUp,
faParking, faSquare, faBus, faHiking, faCircle, faCamera, faCameraHome, faVolume, faVolumeMute, faCog, faCaretDown as fasCaretDown,
faLocationArrow as fasLocationArrow, faInfoCircle as fasInfoCircle } from '@fortawesome/pro-solid-svg-icons'
@ -29,7 +29,7 @@ library.add(faCheck, faCheckCircle, faInfoCircle, faExclamationTriangle, faExcla
faQuoteRight, faSearch, faMountains, faUser, faClock, faChevronCircleUp, faChevronCircleDown, faMap, faChartBar, faFileDownload,
faExchange, faGlobe, faCalendarDay, faTrashAlt, faEdit, faClone, farCheckCircle, faArrowsH, faArrowsAlt,
faSnowflake, faWindowMinimize, faWindowMaximize, faWindowClose, faExpandArrows, faLocation, faCalendarCheck, faComment, faSpinner,
faBookUser)
faBookUser, faBookmark, faTag)
library.add(faMap, fasCheckCircle, fasChevronCircleDown, fasChevronCircleUp, faParking, faSquare, faBus, faHiking, faCircle, faCamera,
faCameraHome, faVolume, faVolumeMute, faCog, fasCaretDown, fasLocationArrow, fasInfoCircle)
library.add(faWikipediaW, faGoogle, faGithub)

Wyświetl plik

@ -10,6 +10,27 @@ export default {
return response.data
})
},
getPersonalData () {
return this.axiosAuth.get('https://api.sotl.as/users/me')
},
postPersonalSettings (key, value) {
return this.axiosAuth.post('https://api.sotl.as/users/me/settings', { [key]: value })
},
getPersonalSummitData (summitCode) {
return this.axiosAuth.get('https://api.sotl.as/users/me/summit/' + summitCode)
},
postPersonalSummitData (summitCode, isBookmarked, notes, tags) {
return this.axiosAuth.post('https://api.sotl.as/users/me/summit/' + summitCode, {
isBookmarked: isBookmarked,
notes: notes,
tags: tags })
},
getPersonalSummitTags () {
return this.axiosAuth.get('https://api.sotl.as/users/me/tags')
},
getPersonalSummitsFromTag (tagName) {
return this.axiosAuth.get('https://api.sotl.as/users/me/summits/tags', { params: { q: tagName } })
},
uploadPhoto (summitCode, file, progress, cancelToken) {
let formData = new FormData()
formData.append('photo', file)

Wyświetl plik

@ -1,4 +1,8 @@
import api from '@/mixins/api'
import utils from '@/mixins/utils'
export default {
mixins: [api, utils],
mounted () {
if (this.$options.prefs) {
this.loadPrefs()
@ -27,7 +31,7 @@ export default {
})
this.setPrefs(this.$options.prefs.key, prefs)
},
getPrefs (key) {
getPrefsFromLocalStorage (key) {
if (localStorage.getItem(key)) {
try {
return JSON.parse(localStorage.getItem(key))
@ -37,8 +41,29 @@ export default {
}
return undefined
},
setPrefs (key, prefs) {
getPrefs (key) {
if (!this.authenticated) {
return this.getPrefsFromLocalStorage(key)
}
this.getPersonalData().then(response => {
if (response[key]) {
this.setPrefsToLocalStorage(key, response[key])
return JSON.parse(response[key])
}
return this.getPrefsFromLocalStorage(key)
}).catch(() => {
return this.getPrefsFromLocalStorage(key)
})
},
setPrefsToLocalStorage (key, prefs) {
localStorage.setItem(key, JSON.stringify(prefs))
},
setPrefs (key, prefs) {
if (this.authenticated) {
this.postPersonalSettings(key, prefs)
}
this.setPrefsToLocalStorage(key, prefs)
}
}
}

Wyświetl plik

@ -240,6 +240,40 @@ export default {
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
},
activationsMapBounds (summitList) {
let minLat, minLon, maxLat, maxLon
summitList.forEach(summitObj => {
if (!minLat || summitObj.summit.coordinates.latitude < minLat) {
minLat = summitObj.summit.coordinates.latitude
}
if (!maxLat || summitObj.summit.coordinates.latitude > maxLat) {
maxLat = summitObj.summit.coordinates.latitude
}
if (!minLon || summitObj.summit.coordinates.longitude < minLon) {
minLon = summitObj.summit.coordinates.longitude
}
if (!maxLon || summitObj.summit.coordinates.longitude > maxLon) {
maxLon = summitObj.summit.coordinates.longitude
}
})
// Some padding
let latDiff = maxLat - minLat
let lonDiff = maxLon - minLon
minLat -= (latDiff * 0.1)
maxLat += (latDiff * 0.1)
minLon -= (lonDiff * 0.1)
maxLon += (lonDiff * 0.1)
return [[minLon, minLat], [maxLon, maxLat]]
},
activationsMapFilter (summitList) {
let summits = new Set()
summitList.forEach(activation => {
summits.add(activation.summit.code)
})
return ['in', 'code', ...summits]
}
}
}

Wyświetl plik

@ -10,6 +10,7 @@ import NotFound from './views/NotFound.vue'
import SearchAnything from './views/SearchAnything.vue'
import Activator from './views/Activator.vue'
import Activators from './views/Activators.vue'
import UserData from './views/UserData.vue'
import Summit from './views/Summit.vue'
import Activation from './views/Activation.vue'
import Spots from './views/Spots.vue'
@ -142,6 +143,11 @@ let router = new Router({
return '/activators/' + to.params.callsign.toUpperCase()
}
},
{
path: '/user_data/',
component: UserData,
meta: { savePath: null }
},
{
path: '/spots',
component: Spots,

Wyświetl plik

@ -4,7 +4,7 @@
<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>
<h4 class="title is-4"><b-icon icon="mountains" />Summits<span v-if="tagSearch"> tagged with <font-awesome-icon :icon="['far', 'tag']" class="faicon" /> {{ this.tagSearch }}</span></h4>
<b-field v-if="inactiveCount > 0" grouped>
<b-switch v-model="showInactive">Show inactive ({{ inactiveCount }})</b-switch>
</b-field>
@ -50,10 +50,28 @@
</div>
</section>
<section v-if="userTags !== null && userTags.length > 0" class="section">
<div class="container">
<h4 class="title is-4"><b-icon icon="tag" />User Tags</h4>
<b-table class="auto-width" default-sort="tag" :narrowed="true" :striped="true" :data="userTags" :mobile-cards="false">
<template slot-scope="props">
<b-table-column field="tag" label="User Tag" class="nowrap" sortable>
<font-awesome-icon :icon="['far', 'tag']" class="faicon" />
<router-link :to="'/search?q=@' + encodeURIComponent(props.row.tag)">{{ props.row.tag }}</router-link>
</b-table-column>
<b-table-column field="count" :label="Count" numeric sortable>
{{ props.row.count }}
</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" type="is-info" has-icon>
No matching summits or activators for '{{ $route.query.q }}' found.
<b-message v-if="activators !== null && activators.length === 0 && summits !== null && summits.length === 0 && userTags !== null && userTags.length === 0" type="is-info" has-icon>
No matching summits, activators or user tags for '{{ $route.query.q }}' found.
</b-message>
</div>
</section>
@ -68,16 +86,51 @@ import utils from '../mixins/utils.js'
import PageLayout from '../components/PageLayout.vue'
import SummitList from '../components/SummitList.vue'
import api from '@/mixins/api'
export default {
name: 'SearchAnything',
components: { PageLayout, SummitList },
mixins: [utils],
mixins: [utils, api],
methods: {
doSearch () {
let loads = []
let q = this.$route.query.q.trim()
this.loadingComponent = this.$buefy.loading.open({ canCancel: true })
if (q.startsWith('@') && this.authenticated) {
this.tagSearch = q.substring(1)
loads.push(this.getPersonalSummitsFromTag(this.tagSearch)
.then(response => {
response.data.forEach(summit => {
summit.isValid = true
})
this.summits = response.data
return this.loadingComponent.close()
}))
} else {
this.tagSearch = null
loads.push(axios.get('https://api.sotl.as/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
}))
}
if (this.authenticated) {
loads.push(this.getPersonalSummitTags()
.then(response => {
this.userTags = response.data.filter(tag => {
return tag.tag.toLowerCase().includes(q.toLowerCase())
})
}))
} else {
this.userTags = []
}
loads.push(axios.get(process.env.VUE_APP_API_URL + '/activators/search', { params: { q, limit: this.limit } })
.then(response => {
this.activators = response.data.activators
@ -138,6 +191,8 @@ export default {
activators: null,
limit: 100,
summits: null,
userTags: null,
tagSearch: null,
showInactive: false
}
}
@ -173,4 +228,12 @@ export default {
.message.is-warning {
margin-top: 1rem;
}
.faicon {
margin-right: 0.5em;
}
.title .faicon {
opacity: 0.5;
margin-left: 0.3em;
margin-right: 0;
}
</style>

Wyświetl plik

@ -4,6 +4,14 @@
<h1 class="title is-size-1 is-size-3-mobile">
{{ summit.name }}
<div class="action-button">
<b-field>
<p class="control">
<b-button :type="bookmarkButtonType" size="is-small" icon-left="bookmark" :outlined="!isBookmarkedActive" @click="toggleBookmark()" :disabled="!authenticated">Bookmark</b-button>
</p>
</b-field>
</div>
<div class="action-button">
<b-field>
<p class="control">
@ -61,13 +69,27 @@
<SummitAttributes :attributes="summit.attributes" />
<div v-if="authenticated">
<h6 class="title is-6">Tags</h6>
<div class="taginput-wrapper">
<b-taginput v-model="summitTags" ref="summitTagsInput" autocomplete open-on-focus allow-new :data="filteredSummitTagsExisting" :confirm-key-codes="[9,13,32,188]" @typing="getFilteredSummitTags" @input="onSummitTagsInput" @blur="onSummitTagsInputBlur" rounded />
</div>
</div>
<template v-if="resources.length > 0">
<h6 class="title is-6">Resources</h6>
<ResourceList :resources="resources" />
</template>
</div>
<div class="column">
<MiniMap :class="{ map: true, enlarge: enlargeMap }" :summit="summit" :routes="routes" :canEnlarge="true" :isEnlarged="enlargeMap" :showInactiveSummits="!isValid" ref="map" @enlarge="toggleEnlargeMap" @photoClicked="photoClicked" />
<div v-if="authenticated">
<h6 class="title is-6">Notes</h6>
<div>
<b-input type="textarea" class="summit-notes" id="textarea-summit-notes" v-model="summitNotes" v-debounce:1s="savePersonalSummitData" placeholder="Your personal summit notes" size="is-small" rows="3"></b-input>
</div>
</div>
</div>
</div>
</div>
@ -99,7 +121,7 @@
<h4 class="title is-4">Photos</h4>
<SummitPhotos ref="summitPhotos" :summit="summit" :editable="true" :showWaypointButton="true" @photoDeleted="reloadPhotos" @photoEdited="reloadPhotos" @photosReordered="reloadPhotos" />
<PhotosUploader v-if="$keycloak && $keycloak.authenticated" :summitCode="summitCode" @upload="reloadPhotos" />
<PhotosUploader v-if="authenticated" :summitCode="summitCode" @upload="reloadPhotos" />
<div v-else class="uploader-placeholder box"><font-awesome-icon :icon="['far', 'images']" size="lg" /> Log in and upload your photos of this summit!</div>
</div>
</section>
@ -150,6 +172,7 @@ import HikrIcon from '../assets/hikr.png'
import SACIcon from '../assets/sac.png'
import SotatrailsIcon from '../assets/sotatrails.png'
import EventBus from '../event-bus'
import api from '../mixins/api.js'
export default {
name: 'Summit',
@ -159,7 +182,7 @@ export default {
components: {
SummitDatabasePageLayout, MiniMap, SummitActivations, SummitAttributes, ResourceList, SummitRoutes, SummitPhotos, SummitVideos, PhotosUploader, Coordinates, Bearing, SummitPointsLabel, AltitudeLabel, SpotsList, AlertsList, EditAlert, EditSpot
},
mixins: [utils, smptracks, coverphoto],
mixins: [api, utils, smptracks, coverphoto],
computed: {
locator () {
if (!this.summit.coordinates) {
@ -309,6 +332,9 @@ export default {
}
})
return videos
},
bookmarkButtonType () {
return (this.isBookmarkedActive ? 'is-success' : 'is-info')
}
},
watch: {
@ -386,6 +412,21 @@ export default {
.then(response => {
this.myChases = response.data
}))
loads.push(this.getPersonalSummitData(this.summitCode)
.then(response => {
this.isBookmarkedActive = response.data.isBookmarked ? response.data.isBookmarked : false
this.summitNotes = response.data.notes ? response.data.notes : ''
this.summitTags = response.data.tags ? response.data.tags : []
}))
loads.push(this.getPersonalSummitTags()
.then(response => {
this.summitTagsExisting = response.data.map(item => {
return item.tag
})
this.filteredSummitTagsExisting = this.summitTagsExisting
}))
}
Promise.all(loads)
@ -407,6 +448,20 @@ export default {
this.loadingComponent.close()
})
},
savePersonalSummitData () {
this.postPersonalSummitData(
this.summitCode,
this.isBookmarkedActive,
this.summitNotes,
this.summitTags
).catch(error => {
console.log(error)
})
},
toggleBookmark () {
this.isBookmarkedActive = !this.isBookmarkedActive
this.savePersonalSummitData()
},
addAlert () {
this.isAddAlertActive = true
},
@ -445,6 +500,31 @@ export default {
},
navbarMenuOpened () {
this.enlargeMap = false
},
getFilteredSummitTags (input) {
this.filteredSummitTagsExisting = this.summitTagsExisting
.filter(element => {
return !this.summitTags.includes(element)
})
.filter(element => {
return element
.toString()
.toLowerCase()
.indexOf(input.toLowerCase()) >= 0
})
},
onSummitTagsInput () {
this.filteredSummitTagsExisting = this.summitTagsExisting
.filter(element => {
return !this.summitTags.includes(element)
})
this.savePersonalSummitData()
},
onSummitTagsInputBlur () {
// Delay to avoid double entry when clicking a tag suggestion
setTimeout(() => {
this.$refs.summitTagsInput.addTag()
}, 100)
}
},
data () {
@ -457,7 +537,12 @@ export default {
isAddAlertActive: false,
isAddSpotActive: false,
enlargeMap: false,
alwaysLoadWikipedia: true
alwaysLoadWikipedia: true,
isBookmarkedActive: false,
summitNotes: null,
summitTags: [],
summitTagsExisting: [],
filteredSummitTagsExisting: this.summitTagsExisting
}
}
}
@ -510,6 +595,7 @@ export default {
margin-right: 0.1em;
opacity: 0.5;
}
>>> .coordinates {
font-weight: bold;
}
@ -585,4 +671,10 @@ export default {
.uploader-placeholder .fa-images {
margin-right: 0.5em;
}
.taginput-wrapper, .summit-notes {
margin-top: 0.7em;
}
.summit-notes textarea {
box-shadow: none;
}
</style>

Wyświetl plik

@ -0,0 +1,125 @@
<template>
<PageLayout>
<template v-slot:title>
<h1 class="title is-size-1 is-size-3-mobile">
My User Data
</h1>
</template>
<div v-if="userSummits && userSummits.length > 0">
<section class="section">
<div class="container">
<h4 class="title is-4"><span>Summit Bookmarks</span></h4>
<b-field>
<FilterInput v-model="filterString" ref="filter" :is-regex="true" />
</b-field>
<template v-if="filteredBookmarks && filteredBookmarks.length > 0">
<SummitList :data="filteredBookmarks" auto-width ignore-validity />
</template>
</div>
</section>
<section class="section">
<div class="container">
<h4 class="title is-4"><span>User Tags</span></h4>
<template v-if="userTags && userTags.length > 0">
<b-table class="auto-width" default-sort="tag" :narrowed="true" :striped="true" :data="userTags" :mobile-cards="false">
<template slot-scope="props">
<b-table-column field="tag" label="User Tag" class="nowrap" sortable>
<font-awesome-icon :icon="['far', 'tag']" class="faicon" />
<router-link :to="'/search?q=@' + encodeURIComponent(props.row.tag)">{{ props.row.tag }}</router-link>
</b-table-column>
<b-table-column field="count" :label="Count" numeric sortable>
{{ props.row.count }}
</b-table-column>
</template>
</b-table>
</template>
</div>
</section>
</div>
<div v-else-if="!authenticated">
<section class="section">
<div class="container">
<b-message type="is-info" has-icon>
Log in to view your summits and tags.
</b-message>
</div>
</section>
</div>
<div v-else>
<section class="section">
<div class="container">
<b-message type="is-info" has-icon>
No bookmarks or tags set, yet.
</b-message>
</div>
</section>
</div>
</PageLayout>
</template>
<script>
import FilterInput from '../components/FilterInput.vue'
import SummitList from '../components/SummitList.vue'
import PageLayout from '@/components/PageLayout'
import utils from '@/mixins/utils'
import api from '@/mixins/api'
export default {
name: 'BookmarkList',
components: {
PageLayout, FilterInput, SummitList
},
mixins: [api, utils],
mounted () {
document.title = 'My User Data - SOTLAS'
this.loadingComponent = this.$buefy.loading.open({ canCancel: true })
let loads = []
loads.push(this.getPersonalData().then(response => {
this.userSummits = response.data.userSummits
}))
loads.push(this.getPersonalSummitTags().then(response => {
this.userTags = response.data
}))
Promise.all(loads)
.finally(() => {
this.loadingComponent.close()
})
},
computed: {
filteredBookmarks () {
return this.userSummits.filter(userSummit => {
if (!userSummit.isBookmarked) {
return false
}
if (this.filterString) {
return userSummit.summit.code.includes(this.filterString.toUpperCase()) || userSummit.summit.name.toLowerCase().includes(this.filterString.toLowerCase())
} else {
return true
}
}).map(userSummit => {
return userSummit.summit
})
}
},
data () {
return {
userSummits: [],
showMap: false,
filterString: '',
userTags: []
}
}
}
</script>
<style scoped>
.faicon {
margin-right: 0.5em;
}
</style>