Merge branch 'dev' into feat/add_user_theme_view_controls

This commit is contained in:
lesion
2023-02-03 15:18:16 +01:00
117 changed files with 4890 additions and 2443 deletions

View File

@@ -1,22 +1,20 @@
<template>
<nav>
<NavHeader/>
<NavHeader />
<!-- title -->
<div class='text-center'>
<nuxt-link id='title' v-text='settings.title' to='/' />
<div class='text-body-1 font-weight-light' v-text='settings.description' />
<div class="text-center">
<nuxt-link id="title" v-text="settings.title" to="/" />
<div
class="text-body-1 font-weight-light"
v-text="settings.description"
/>
</div>
<NavSearch />
<NavBar />
</nav>
</template>
<script>
import { mapState } from 'vuex'
@@ -27,18 +25,24 @@ import NavSearch from './NavSearch.vue'
export default {
name: 'Appbar',
components: { NavHeader, NavBar, NavSearch },
computed: mapState(['settings'])
computed: mapState(['settings']),
}
</script>
<style>
nav {
background-image: linear-gradient(rgba(59, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), url(/headerimage.png);
background-image: linear-gradient(rgba(0, 0, 0, 0.8), rgba(20, 20, 20, 0.7)),
url(/headerimage.png);
background-position: center center;
background-size: cover;
}
.theme--light nav {
background-image: linear-gradient(to bottom, rgba(255,230,230,.95), rgba(250,250,250,.95)), url(/headerimage.png);
background-image: linear-gradient(
to bottom,
rgba(230, 230, 230, 0.95),
rgba(250, 250, 250, 0.95)
),
url(/headerimage.png);
}
#title {
@@ -46,5 +50,4 @@ nav {
font-weight: 600;
text-decoration: none;
}
</style>
</style>

View File

@@ -15,13 +15,14 @@
aria-label='Calendar'
is-expanded
is-inline)
template(v-slot="{ inputValue, inputEvents }")
//- template(v-slot="{ inputValue, inputEvents }")
v-btn#calendarButton(v-on='inputEvents' text tile :color='selectedDate ? "primary" : "" ') {{inputValue || $t('common.calendar')}}
v-icon(v-if='selectedDate' v-text='mdiClose' right small icon @click.prevent.stop='selectedDate = null')
v-icon(v-else v-text='mdiChevronDown' right small icon)
template(v-slot:placeholder)
v-btn#calendarButton(text tile) {{$t('common.calendar')}}
v-icon(v-text='mdiChevronDown' right small icon)
.calh.d-flex.justify-center.align-center(slot='placeholder')
v-progress-circular(indeterminate)
//- v-btn#calendarButton(text tile) {{$t('common.calendar')}}
//- v-icon(v-text='mdiChevronDown' right small icon)
</template>
@@ -65,6 +66,16 @@ export default {
</script>
<style>
.vc-container.vc-is-dark {
--gray-900: #111;
--gray-700: #333;
}
.vc-container {
--gray-400: #999 !important;
--rounded-lg: 4px !important;
}
.vc-opacity-0 {
opacity: 0.3 !important;
}

View File

@@ -9,7 +9,7 @@ v-dialog(v-model='show'
@keydown.esc='cancel')
v-card
v-card-title {{ title }}
v-card-text(v-show='!!message') {{ message }}
v-card-text(v-show='!!message' v-html='message')
v-card-actions
v-spacer
v-btn(outlined color='error' @click='cancel') {{$t('common.cancel')}}

View File

@@ -3,12 +3,11 @@ v-col(cols=12)
.text-center
v-btn-toggle.v-col-6.flex-column.flex-sm-row(v-model='type' color='primary' @change='type => change("type", type)')
v-btn(value='normal' label="normal") {{ $t('event.normal') }}
v-btn(value='multidate' label='multidate') {{ $t('event.multidate') }}
v-btn(v-if='settings.allow_multidate_event' value='multidate' label='multidate') {{ $t('event.multidate') }}
v-btn(v-if='settings.allow_recurrent_event' value='recurrent' label="recurrent") {{ $t('event.recurrent') }}
p {{ $t(`event.${type}_description`) }}
v-btn-toggle.v-col-6.flex-column.flex-sm-row(v-if='type === "recurrent"' color='primary' :value='value.recurrent.frequency' @change='fq => change("frequency", fq)')
v-btn(v-for='f in frequencies' :key='f.value' :value='f.value') {{ f.text }}
@@ -25,8 +24,9 @@ v-col(cols=12)
is-inline
is-expanded
:min-date='type !== "recurrent" && new Date()')
template(#placeholder)
span.calc Loading
//- template(#placeholder)
.d-flex.calh.justify-center(slot='placeholder')
v-progress-circular(indeterminate)
div.text-center.mb-2(v-if='type === "recurrent"')
span(v-if='value.recurrent.frequency !== "1m" && value.recurrent.frequency !== "2m"') {{ whenPatterns }}
@@ -60,7 +60,7 @@ v-col(cols=12)
:allowedMinutes='allowedMinutes'
format='24hr'
@click:minute='menuFromHour = false'
@change='hr => change("fromHour", hr)')
@input='hr => change("fromHour", hr)')
v-col.col-12.col-sm-6
@@ -88,14 +88,14 @@ v-col(cols=12)
:allowedMinutes='allowedMinutes'
format='24hr'
@click:minute='menuDueHour = false'
@change='hr => change("dueHour", hr)')
@input='hr => change("dueHour", hr)')
List(v-if='type === "normal" && todayEvents.length' :events='todayEvents' :title='$t("event.same_day")')
</template>
<script>
import dayjs from 'dayjs'
import { mapState } from 'vuex'
import { mapState, mapActions } from 'vuex'
import List from '@/components/List'
import { attributesFromEvents } from '../assets/helper'
import { mdiClockTimeFourOutline, mdiClockTimeEightOutline, mdiClose } from '@mdi/js'
@@ -114,7 +114,6 @@ export default {
menuFromHour: false,
menuDueHour: false,
type: this.value.type || 'normal',
events: [],
frequencies: [
{ value: '1w', text: this.$t('event.each_week') },
{ value: '2w', text: this.$t('event.each_2w') },
@@ -123,7 +122,7 @@ export default {
}
},
computed: {
...mapState(['settings']),
...mapState(['settings', 'events']),
fromDate () {
if (this.value.from) {
if (this.value.multidate) {
@@ -139,7 +138,7 @@ export default {
return this.events.filter(e => e.start_datetime >= start && e.start_datetime <= end)
},
attributes() {
return attributesFromEvents(this.events)
return attributesFromEvents(this.events.filter(e => e.id !== this.event.id))
},
whenPatterns() {
if (!this.value.from) { return }
@@ -193,13 +192,12 @@ export default {
} else {
this.type = 'normal'
}
this.events = await this.$api.getEvents({
start: dayjs().unix(),
show_recurrent: true
})
this.events = this.events.filter(e => e.id !== this.event.id)
if (!this.events) {
this.getEvents()
}
},
methods: {
...mapActions(['getEvents']),
updateRecurrent(value) {
this.$emit('input', { ...this.value, recurrent: value || null })
},
@@ -235,6 +233,15 @@ export default {
} else if (what === 'dueHour') {
if (value) {
this.value.due = this.value.due ? this.value.due : this.value.from
const [hour, minute] = value.split(':')
const [fromHour, fromMinute] = this.value.fromHour.split(':')
if (!this.value.multidate) {
if (hour < fromHour) {
this.value.due = dayjs(this.value.from).add(1, 'day').toDate()
} else {
this.value.due = dayjs(this.value.from).toDate()
}
}
} else {
this.value.due = null
}

View File

@@ -50,8 +50,8 @@ export default {
data ({ $store }) {
return {
mdiWalk, mdiBike, mdiCar, mdiMapMarker,
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: '<a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors',
url: $store.state.settings.tilelayer_provider || 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: $store.state.settings.tilelayer_provider_attribution || "<a target=\"_blank\" href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors",
zoom: 14,
center: [this.event.place.latitude, this.event.place.longitude],
marker: {

View File

@@ -1,19 +1,51 @@
<template lang="pug">
#navsearch.mt-2.mt-sm-4(v-if='showCollectionsBar || showSearchBar')
v-text-field.mx-2(v-if='showSearchBar' outlined dense hide-details :placeholder='$t("common.search")' :append-icon='mdiMagnify' @input='search' clearable :clear-icon='mdiClose')
template(v-slot:prepend-inner)
Calendar(v-if='!settings.hide_calendar')
v-btn.ml-2.mt-2.gap-2(v-if='showCollectionsBar' small outlined v-for='collection in collections' color='primary' :key='collection.id' :to='`/collection/${encodeURIComponent(collection.name)}`') {{collection.name}}
#navsearch.mt-2.mt-sm-4(v-if='showCollectionsBar || showSearchBar || showCalendar')
div.mx-2
client-only(v-if='showSearchBar')
v-menu(offset-y :close-on-content-click='false' tile)
template(v-slot:activator="{on ,attrs}")
v-text-field(hide-details outlined
:placeholder='$t("common.search")'
@input="v => setFilter(['query', v])" clearable :clear-icon='mdiClose')
template(v-slot:append v-if='settings.allow_recurrent_event || settings.allow_multidate_event')
v-icon(v-text='mdiCog' v-bind='attrs' v-on='on')
v-card(outlined :rounded='"0"')
v-card-text
v-row(dense)
v-col(v-if='settings.allow_recurrent_event')
v-switch.mt-0(v-model='show_recurrent' @change="v => setFilter(['show_recurrent', v])"
hide-details :label="$t('event.show_recurrent')" inset)
v-col(v-if='settings.allow_multidate_event')
v-switch.mt-0(v-model='show_multidate' @change="v => setFilter(['show_multidate', v])"
hide-details :label="$t('event.show_multidate')" inset)
v-row(v-if='!showCalendar')
v-col
Calendar.mt-2
v-text-field(slot='placeholder' outlined hide-details :placeholder="$t('common.search')" :append-icon='mdiCog')
span(v-if='showCollectionsBar')
v-btn.mr-2.mt-2(small outlined v-for='collection in collections'
color='primary' :key='collection.id'
:to='`/collection/${encodeURIComponent(collection.name)}`') {{collection.name}}
Calendar.mt-2(v-if='showCalendar')
</template>
<script>
import { mapState } from 'vuex'
import { mapState, mapActions } from 'vuex'
import Calendar from '@/components/Calendar'
import { mdiMagnify, mdiClose } from '@mdi/js'
import { mdiClose, mdiCog } from '@mdi/js'
export default {
data: () => ({
mdiMagnify, mdiClose,
collections: []
data: ({ $store }) => ({
oldRoute: '',
mdiClose, mdiCog,
collections: [],
show_recurrent: $store.state.settings.recurrent_event_visible,
show_multidate: true,
query: ''
}),
async fetch () {
this.collections = await this.$axios.$get('collections').catch(_e => [])
@@ -23,21 +55,27 @@ export default {
showSearchBar () {
return this.$route.name === 'index'
},
showCollectionsBar () {
return ['index', 'collection-collection'].includes(this.$route.name)
showCalendar () {
return (!this.settings.hide_calendar && this.$route.name === 'index')
},
...mapState(['settings'])
showCollectionsBar () {
const show = ['index', 'collection-collection'].includes(this.$route.name)
if (show && this.oldRoute !== this.$route.name) {
this.oldRoute = this.$route.name
this.$fetch()
}
return show
},
...mapState(['settings', 'filter'])
},
methods: {
search (ev) {
this.$root.$emit('search', ev)
}
...mapActions(['setFilter']),
}
}
</script>
<style>
#navsearch {
margin: 0 auto;
max-width: 800px;
max-width: 700px;
}
</style>
</style>

View File

@@ -22,16 +22,19 @@ v-row.mb-4
v-list-item-title(v-text='item.name')
v-list-item-subtitle(v-text='item.address')
//- v-text-field(
//- ref='address'
//- :prepend-icon='mdiMap'
//- :disabled='disableAddress'
//- :rules="[ v => disableAddress ? true : $validators.required('common.address')(v)]"
//- :label="$t('common.address')"
//- @change="changeAddress"
//- :value="value.address")
v-col(cols=12 md=6)
v-combobox(ref='address'
v-text-field(v-if="!settings.allow_geolocation"
ref='address'
:prepend-icon='mdiMap'
:disabled='disableAddress'
:rules="[ v => disableAddress ? true : $validators.required('common.address')(v)]"
:label="$t('common.address')"
:hint="$t('event.address_description')"
persistent-hint
@change="changeAddress"
:value="value.address")
v-combobox(ref='address' v-else
:prepend-icon='mdiMapSearch'
:disabled='disableAddress'
@input.native='searchAddress'
@@ -44,9 +47,9 @@ v-row.mb-4
@change='selectAddress'
@focus='searchAddress'
:items="addressList"
:hint="$t('event.address_description' + (settings.allow_geolocation && '_osm'))")
:hint="$t('event.address_description_osm')")
template(v-slot:message="{message, key}")
span(v-html='message' :key="key")
span(v-html='message' :key="key")
template(v-slot:item="{ item, attrs, on }")
v-list-item(v-bind='attrs' v-on='on')
v-icon.pr-4(v-text='loadCoordinatesResultIcon(item)')
@@ -76,7 +79,7 @@ export default {
props: {
value: { type: Object, default: () => ({}) }
},
data () {
data ( {$store} ) {
return {
mdiMap, mdiMapMarker, mdiPlus, mdiMapSearch, mdiLatitude, mdiLongitude, mdiRoadVariant, mdiHome, mdiCityVariant,
place: { },
@@ -91,7 +94,14 @@ export default {
node: mdiMapMarker,
relation: mdiCityVariant,
},
nominatim_class: ['amenity', 'shop', 'tourism', 'leisure', 'building']
nominatim_class: ['amenity', 'shop', 'tourism', 'leisure', 'building'],
photon_osm_key: ['amenity', 'shop', 'tourism', 'leisure', 'building'],
photon_osm_type: {
'W': mdiRoadVariant,
'N': mdiMapMarker,
'R': mdiCityVariant,
},
geocoding_provider_type: $store.state.settings.geocoding_provider_type || 'Nominatim'
}
},
computed: {
@@ -115,21 +125,33 @@ export default {
return matches
}
},
mounted () {
this.$nextTick( () => {
this.search()
})
},
methods: {
search: debounce(async function(ev) {
const search = ev.target.value.trim().toLowerCase()
const search = ev ? ev.target.value.trim().toLowerCase() : ''
this.places = await this.$axios.$get(`place?search=${search}`)
if (!search && this.places.length) { return this.places }
const matches = this.places.find(p => search === p.name.toLocaleLowerCase())
if (!matches && search) {
this.places.unshift({ create: true, name: ev.target.value.trim() })
}
}, 100),
}, 200),
loadCoordinatesResultIcon(item) {
if ( this.nominatim_class.includes(item.class)) {
return this.mdiHome
if (this.geocoding_provider_type == "Nominatim") {
if ( this.nominatim_class.includes(item.class)) {
return this.mdiHome
}
return this.nominatim_osm_type[item.type]
} else if (this.geocoding_provider_type == "Photon") {
if ( this.photon_osm_key.includes(item.class)) {
return this.mdiHome
}
return this.photon_osm_type[item.type]
}
return this.nominatim_osm_type[item.type]
},
selectPlace (p) {
if (!p) { return }
@@ -168,11 +190,11 @@ export default {
}
this.$emit('input', { ...this.place })
},
// changeAddress (v) {
// this.place.address = v
// this.$emit('input', { ...this.place })
// this.disableDetails = false
// },
changeAddress (v) {
this.place.address = v
this.$emit('input', { ...this.place })
this.disableDetails = false
},
selectAddress (v) {
if (!v) { return }
if (typeof v === 'object') {
@@ -220,22 +242,55 @@ export default {
if (searchCoordinates.length) {
this.loading = true
const ret = await this.$axios.$get(`placeNominatim/${searchCoordinates}`)
if (ret && ret.length) {
this.addressList = ret.map(v => {
const name = get(v.namedetails, 'alt_name', get(v.namedetails, 'name'))
const address = v.display_name ? v.display_name.replace(name, '').replace(/^, ?/, '') : ''
return {
class: v.class,
type: v.osm_type,
lat: v.lat,
lon: v.lon,
name,
address
}
})
} else {
this.addressList = []
const ret = await this.$axios.$get(`placeOSM/${this.geocoding_provider_type}/${searchCoordinates}`)
if (this.geocoding_provider_type == "Nominatim") {
if (ret && ret.length) {
this.addressList = ret.map(v => {
const name = get(v.namedetails, 'alt_name', get(v.namedetails, 'name'))
const address = v.display_name ? v.display_name.replace(name, '').replace(/^, ?/, '') : ''
return {
class: v.class,
type: v.osm_type,
lat: v.lat,
lon: v.lon,
name,
address
}
})
} else {
this.addressList = []
}
} else if (this.geocoding_provider_type == "Photon") {
let photon_properties = ['housenumber', 'street', 'locality', 'district', 'city', 'county', 'state', 'postcode', 'country']
if (ret) {
this.addressList = ret.features.map(v => {
let pre_name = v.properties.name || v.properties.street || ''
let pre_address = ''
photon_properties.forEach((item, i) => {
let last = i == (photon_properties.length - 1)
if (v.properties[item] && !last) {
pre_address += v.properties[item]+', '
} else if (v.properties[item]) {
pre_address += v.properties[item]
}
});
let name = pre_name
let address = pre_address
return {
class: v.properties.osm_key,
type: v.properties.osm_type,
lat: v.geometry.coordinates[1],
lon: v.geometry.coordinates[0],
name,
address
}
})
} else {
this.addressList = []
}
}
this.loading = false
}

View File

@@ -33,7 +33,7 @@ v-container
:prepend-icon="mdiTagMultiple"
chips small-chips multiple deletable-chips hide-no-data hide-selected persistent-hint
:disabled="!collection.id"
placeholder='Tutte'
placeholder='All'
@input.native='searchTags'
@focus='searchTags'
:delimiters="[',', ';']"
@@ -69,7 +69,7 @@ v-container
//- v-list-item-subtitle(v-text='item.address')
v-col(cols=2)
v-btn(color='primary' text @click='addFilter' :disabled='!collection.id || !filterPlaces.length && !filterTags.length') add <v-icon v-text='mdiPlus'></v-icon>
v-btn(color='primary' :loading='loading' text @click='addFilter' :disabled='loading || !collection.id || !filterPlaces.length && !filterTags.length') add <v-icon v-text='mdiPlus'></v-icon>
v-data-table(
:headers='filterHeaders'
@@ -110,6 +110,9 @@ v-container
<script>
import get from 'lodash/get'
import debounce from 'lodash/debounce'
import isEqual from 'lodash/isEqual'
import sortBy from 'lodash/sortBy'
import { mdiPencil, mdiChevronLeft, mdiChevronRight, mdiMagnify, mdiPlus, mdiTagMultiple, mdiMapMarker, mdiDeleteForever, mdiCloseCircle, mdiChevronDown } from '@mdi/js'
export default {
@@ -147,7 +150,7 @@ export default {
methods: {
searchTags: debounce(async function (ev) {
this.tags = await this.$axios.$get(`/tag?search=${ev.target.value}`)
this.tags = await this.$axios.$get(`/tag?search=${encodeURIComponent(ev.target.value)}`)
}, 100),
searchPlaces: debounce(async function (ev) {
this.places = await this.$axios.$get(`/place?search=${ev.target.value}`)
@@ -163,9 +166,20 @@ export default {
this.loading = true
const tags = this.filterTags
const places = this.filterPlaces.map(p => ({ id: p.id, name: p.name }))
const filter = await this.$axios.$post('/filter', { collectionId: this.collection.id, tags, places })
const filter = { collectionId: this.collection.id, tags, places }
// tags and places are JSON field and there's no way to use them inside a unique constrain
//
const alreadyExists = this.filters.find(f =>
isEqual(sortBy(f.places, 'id'), sortBy(filter.places, 'id')) && isEqual(sortBy(f.tags), sortBy(filter.tags))
)
if (alreadyExists) return
const ret = await this.$axios.$post('/filter', filter )
this.$fetch()
this.filters.push(filter)
this.filters.push(ret)
this.filterTags = []
this.filterPlaces = []
this.loading = false

View File

@@ -0,0 +1,169 @@
<template lang="pug">
v-card
v-card-title {{$t('admin.geolocation')}}
v-card-text
p.mb-6(v-html="$t('admin.geolocation_description')")
v-form
v-row
v-col(md=3)
v-autocomplete.mb-4(v-model='geocoding_provider_type'
@blur="save('geocoding_provider_type', geocoding_provider_type )"
:label="$t('admin.geocoding_provider_type')"
:hint="$t('admin.geocoding_provider_type_help')"
persistent-hint
:items="geocoding_provider_type_items"
:placeholder="geocoding_provider_type_default")
v-col(md=5)
v-text-field.mb-4(v-model='geocoding_provider'
@blur="save('geocoding_provider', geocoding_provider )"
:label="$t('admin.geocoding_provider')"
:hint="$t('admin.geocoding_provider_help')"
persistent-hint
:placeholder="geocoding_provider_default")
v-col(md=4)
v-autocomplete.mb-6(v-model="geocoding_countrycodes" :disabled="!(geocoding_provider_type === null || geocoding_provider_type === 'Nominatim')"
:append-icon='mdiChevronDown'
@blur="save('geocoding_countrycodes', geocoding_countrycodes )"
:label="$t('admin.geocoding_countrycodes')"
:items="countries"
multiple chips small-chips persistent-hint
item-value="code"
item-text="name"
:hint="$t('admin.geocoding_countrycodes_help')")
v-row
v-col(md=6)
v-text-field.mb-4(v-model='tilelayer_provider'
@blur="save('tilelayer_provider', tilelayer_provider )"
:label="$t('admin.tilelayer_provider')"
:hint="$t('admin.tilelayer_provider_help')"
persistent-hint
:placeholder="tilelayer_provider_default")
v-col(md=6)
v-text-field(v-model='tilelayer_provider_attribution'
@blur="save('tilelayer_provider_attribution', tilelayer_provider_attribution )"
:label="$t('admin.tilelayer_provider_attribution')"
:placeholder="tilelayer_provider_attribution_default")
div(id="leaflet-map-preview" max-height='10px')
//- Map
v-card-actions
v-spacer
v-btn(color='primary' @click='testGeocodingProvider' :loading='testGeocodingLoading' outlined ) {{$t('admin.geocoding_test_button')}}
v-btn(color='primary' @click='testTileLayerProvider' :loading='testTileLayerLoading' outlined ) {{$t('admin.tilelayer_test_button')}}
</template>
<script>
import { mapActions, mapState } from 'vuex'
import { isoCountries } from '../../server/helpers/geolocation'
import { mdiChevronDown } from '@mdi/js'
// import Map from '~/components/Map'
import "leaflet/dist/leaflet.css"
export default {
props: {
setup: { type: Boolean, default: false }
},
// components: { Map },
data ({ $store }) {
return {
mdiChevronDown,
loading: false,
testGeocodingLoading: false,
testTileLayerLoading: false,
geocoding_provider_type_items: ['Nominatim', 'Photon'],
geocoding_provider_type: $store.state.settings.geocoding_provider_type || '',
geocoding_provider_type_default: 'Nominatim',
geocoding_provider: $store.state.settings.geocoding_provider || '',
geocoding_provider_default: "https://nominatim.openstreetmap.org/search" ,
geocoding_countrycodes: $store.state.settings.geocoding_countrycodes || [],
tilelayer_provider: $store.state.settings.tilelayer_provider || '',
tilelayer_provider_default: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
tilelayer_provider_attribution: $store.state.settings.tilelayer_provider_attribution || '',
tilelayer_provider_attribution_default: '<a target=\'_blank\' href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors',
countries: isoCountries,
mapPreviewTest: null,
}
},
created() {
if (process.client) {
const L = require('leaflet')
}
},
computed: mapState(['settings', 'events']),
methods: {
...mapActions(['setSetting']),
async testGeocodingProvider () {
this.testGeocodingLoading = true
const geocodingProviderTest = this.geocoding_provider || this.geocoding_provider_default
const geocodingSoftwareTest = this.geocoding_provider_type || this.geocoding_provider_type_default
const geocodingQuery = 'building'
try {
if (geocodingSoftwareTest === 'Nominatim') {
const geolocation = await this.$axios.$get(`${geocodingProviderTest}`, {timeout: 3000, params: {q: `${geocodingQuery}`, format: 'json', limit: 1 }} )
} else if (geocodingSoftwareTest === 'Photon') {
const geolocation = await this.$axios.$get(`${geocodingProviderTest}`, {timeout: 3000, params: {q: `${geocodingQuery}`, limit: 1}} )
}
this.$root.$message(this.$t('admin.geocoding_test_success', { service_name: geocodingProviderTest }), { color: 'success' })
} catch (e) {
this.$root.$message(this.$t('admin.tilelayer_test_error', { service_name: geocodingProviderTest }), { color: 'error' })
}
this.testGeocodingLoading = false
},
async testTileLayerProvider () {
this.testTileLayerLoading = true
const tileThis = this
const tileLayerTest = this.tilelayer_provider || this.tilelayer_provider_default
const tileLayerAttributionTest = this.tilelayer_provider_attribution || this.tilelayer_provider_attribution_default
// init tilelayer
if (this.mapPreviewTest == null) {
this.mapPreviewTest = L.map("leaflet-map-preview").setView([40,40],10);
}
this.tileLayer = L.tileLayer(`${tileLayerTest}`, {attribution: `${tileLayerAttributionTest}`})
this.tileLayer.addTo(this.mapPreviewTest)
// tilelayer events inherited from gridlayer https://leafletjs.com/reference.html#gridlayer
this.tileLayer.on('tileload', function (event) {
tileThis.tileLayerTestSucess(event, tileLayerTest)
});
this.tileLayer.on('tileerror', function(error, tile) {
tileThis.tileLayerTestError(event, tileLayerTest)
tileThis.tileLayer = null
});
this.testTileLayerLoading = false
},
save (key, value) {
if (this.settings[key] !== value) {
this.setSetting({ key, value })
}
},
done () {
this.$emit('close')
},
geocodingTestError(event, tileLayerTest) {
this.$root.$message(this.$t('admin.geocoding_test_error', { service_name: geocodingTest }), { color: 'error' })
},
tileLayerTestSucess(event, tileLayerTest) {
this.$root.$message(this.$t('admin.tilelayer_test_success', { service_name: tileLayerTest }), { color: 'success' })
},
tileLayerTestError(event, tileLayerTest) {
this.$root.$message(this.$t('admin.tilelayer_test_error', { service_name: tileLayerTest }), { color: 'error' })
}
}
}
</script>
<style>
#leaflet-map-preview {
height: 20rem;
}
</style>

View File

@@ -68,7 +68,7 @@ import debounce from 'lodash/debounce'
import get from 'lodash/get'
export default {
data() {
data( {$store} ) {
return {
mdiPencil, mdiChevronRight, mdiChevronLeft, mdiMagnify, mdiEye, mdiMapSearch, mdiChevronDown,
loading: false,
@@ -84,11 +84,12 @@ export default {
{ value: 'address', text: this.$t('common.address') },
{ value: 'map', text: 'Map' },
{ value: 'actions', text: this.$t('common.actions'), align: 'right' }
]
],
geocoding_provider_type: $store.state.settings.geocoding_provider_type || 'Nominatim'
}
},
async fetch() {
this.places = await this.$axios.$get('/place/all')
this.places = await this.$axios.$get('/places')
},
computed: {
...mapState(['settings']),
@@ -124,7 +125,7 @@ export default {
this.place.latitude = this.place.longitude = null
}
this.$emit('input', { ...this.place })
},
},
searchAddress: debounce(async function(ev) {
const pre_searchCoordinates = ev.target.value.trim().toLowerCase()
// allow pasting coordinates lat/lon and lat,lon
@@ -159,24 +160,59 @@ export default {
if (searchCoordinates.length) {
this.loading = true
const ret = await this.$axios.$get(`placeNominatim/${searchCoordinates}`)
if (ret && ret.length) {
this.addressList = ret.map(v => {
const name = get(v.namedetails, 'alt_name', get(v.namedetails, 'name'))
const address = v.display_name ? v.display_name.replace(name, '').replace(/^, ?/, '') : ''
return {
lat: v.lat,
lon: v.lon,
name,
address
}
})
} else {
this.addressList = []
const ret = await this.$axios.$get(`placeOSM/${this.geocoding_provider_type}/${searchCoordinates}`)
if (this.geocoding_provider_type == "Nominatim") {
if (ret && ret.length) {
this.addressList = ret.map(v => {
const name = get(v.namedetails, 'alt_name', get(v.namedetails, 'name'))
const address = v.display_name ? v.display_name.replace(name, '').replace(/^, ?/, '') : ''
return {
class: v.class,
type: v.osm_type,
lat: v.lat,
lon: v.lon,
name,
address
}
})
} else {
this.addressList = []
}
} else if (this.geocoding_provider_type == "Photon") {
let photon_properties = ['housenumber', 'street', 'district', 'city', 'county', 'state', 'postcode', 'country']
if (ret) {
this.addressList = ret.features.map(v => {
let pre_name = v.properties.name || v.properties.street || ''
let pre_address = ''
photon_properties.forEach((item, i) => {
let last = i == (photon_properties.length - 1)
if (v.properties[item] && !last) {
pre_address += v.properties[item]+', '
} else if (v.properties[item]) {
pre_address += v.properties[item]
}
});
let name = pre_name
let address = pre_address
return {
class: v.properties.osm_key,
type: v.properties.osm_type,
lat: v.geometry.coordinates[1],
lon: v.geometry.coordinates[0],
name,
address
}
})
} else {
this.addressList = []
}
}
this.loading = false
}
}, 300)
}, 300)
}
}
</script>

View File

@@ -7,7 +7,9 @@ v-card
v-text-field(v-model='admin_email'
@blur="save('admin_email', admin_email )"
:label="$t('admin.sender_email')"
:label="$t('admin.admin_email')"
:hint="$t('admin.admin_email_help')"
persistent-hint
:rules="$validators.email")
v-switch(v-model='smtp.sendmail'

View File

@@ -39,6 +39,10 @@ v-container
inset
:label="$t('admin.allow_anon_event')")
v-switch.mt-1(v-model='allow_multidate_event'
inset
:label="$t('admin.allow_multidate_event')")
v-switch.mt-1(v-model='allow_recurrent_event'
inset
:label="$t('admin.allow_recurrent_event')")
@@ -57,32 +61,35 @@ v-container
v-card-actions
v-btn(text @click='showSMTP=true')
<v-icon v-if='!settings.admin_email' color='error' v-text='mdiAlert'></v-icon> {{$t('admin.show_smtp_setup')}}
v-btn(text @click='$emit("complete")' color='primary' v-if='setup') {{$t('common.next')}}
v-icon(v-text='mdiArrowRight')
<v-icon v-if='!settings.admin_email' color='error' class="mr-2" v-text='mdiAlert'></v-icon> {{$t('admin.show_smtp_setup')}}
v-btn(text @click='$emit("complete")' color='primary' v-if='setup') {{$t('common.next')}}
v-icon(v-text='mdiArrowRight')
</template>
<script>
import SMTP from './SMTP.vue'
import Geolocation from './Geolocation.vue'
import { mapActions, mapState } from 'vuex'
import moment from 'dayjs'
import tzNames from './tz.json'
import { mdiAlert, mdiArrowRight } from '@mdi/js'
import { mdiAlert, mdiArrowRight, mdiMap } from '@mdi/js'
const locales = require('../../locales/index')
export default {
props: {
setup: { type: Boolean, default: false }
},
components: { SMTP },
components: { SMTP, Geolocation },
name: 'Settings',
data ({ $store }) {
return {
mdiAlert, mdiArrowRight,
mdiAlert, mdiArrowRight, mdiMap,
title: $store.state.settings.title,
description: $store.state.settings.description,
locales: Object.keys(locales).map(locale => ({ value: locale, text: locales[locale] })),
showSMTP: false,
showGeolocationConfigs: false,
}
},
computed: {
@@ -107,6 +114,10 @@ export default {
get () { return this.settings.allow_recurrent_event },
set (value) { this.setSetting({ key: 'allow_recurrent_event', value }) }
},
allow_multidate_event: {
get () { return this.settings.allow_multidate_event },
set (value) { this.setSetting({ key: 'allow_multidate_event', value }) }
},
recurrent_event_visible: {
get () { return this.settings.recurrent_event_visible },
set (value) { this.setSetting({ key: 'recurrent_event_visible', value }) }

114
components/admin/Tags.vue Normal file
View File

@@ -0,0 +1,114 @@
<template lang='pug'>
v-container
v-card-title {{ $t('common.tags') }}
v-spacer
v-text-field(v-model='search'
:append-icon='mdiMagnify' outlined rounded
:label="$t('common.search')"
single-line hide-details)
v-dialog(v-model='dialog' width='600' :fullscreen='$vuetify.breakpoint.xsOnly')
v-card
v-card-title {{$t('admin.edit_tag')}} -
strong.ml-2 {{tag.tag}}
v-card-subtitle {{$tc('admin.edit_tag_help', tag.count)}}
v-card-text
v-form(v-model='valid' ref='form' lazy-validation)
v-combobox(v-model='newTag'
:prepend-icon="mdiTag"
hide-no-data
persistent-hint
:items="tags"
:return-object='false'
item-value='tag'
item-text='tag'
:label="$t('common.tags')")
template(v-slot:item="{ item, on, attrs }")
span "{{item.tag}}" <small>({{item.count}})</small>
v-card-actions
v-spacer
v-btn(@click='dialog = false' outlined color='warning') {{ $t('common.cancel') }}
v-btn(@click='saveTag' color='primary' outlined :loading='loading'
:disable='!valid || loading') {{ $t('common.save') }}
v-card-text
v-data-table(
:headers='headers'
:items='tags'
:hide-default-footer='tags.length < 5'
:header-props='{ sortIcon: mdiChevronDown }'
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
:search='search')
template(v-slot:item.map='{ item }')
span {{item.latitude && item.longitude && 'YEP' }}
template(v-slot:item.actions='{ item }')
v-btn(@click='editTag(item)' color='primary' icon)
v-icon(v-text='mdiPencil')
nuxt-link(:to='`/tag/${item.tag}`')
v-icon(v-text='mdiEye')
v-btn(@click='removeTag(item)' color='primary' icon)
v-icon(v-text='mdiDeleteForever')
</template>
<script>
import { mdiPencil, mdiChevronLeft, mdiChevronRight, mdiMagnify, mdiEye, mdiMapSearch, mdiChevronDown, mdiDeleteForever, mdiTag } from '@mdi/js'
import { mapState } from 'vuex'
import get from 'lodash/get'
export default {
data() {
return {
mdiPencil, mdiChevronRight, mdiChevronLeft, mdiMagnify, mdiEye, mdiMapSearch, mdiChevronDown, mdiDeleteForever, mdiTag,
loading: false,
dialog: false,
valid: false,
tag: {},
newTag: '',
tags: [],
search: '',
headers: [
{ value: 'tag', text: this.$t('common.tag') },
{ value: 'count', text: 'N.' },
{ value: 'actions', text: this.$t('common.actions'), align: 'right' }
]
}
},
async fetch() {
this.tags = await this.$axios.$get('/tags')
},
computed: {
...mapState(['settings']),
},
methods: {
editTag(item) {
this.tag.tag = item.tag
this.tag.count = item.count
this.dialog = true
},
async saveTag() {
if (!this.$refs.form.validate()) return
this.loading = true
this.$nextTick( async () => {
await this.$axios.$put('/tag', { tag: this.tag.tag, newTag: this.newTag })
await this.$fetch()
this.newTag = ''
this.loading = false
this.dialog = false
})
},
async removeTag(tag) {
const ret = await this.$root.$confirm('admin.delete_tag_confirm', { tag: tag.tag, n: tag.count })
if (!ret) { return }
try {
await this.$axios.$delete(`/tag/${encodeURIComponent(tag.tag)}`)
await this.$fetch()
} catch (e) {
const err = get(e, 'response.data.errors[0].message', e)
this.$root.$message(this.$t(err), { color: 'error' })
this.loading = false
}
}
}
}
</script>

View File

@@ -16,6 +16,7 @@ v-container
:label="$t('admin.hide_calendar')")
v-card-title {{$t('admin.default_images')}}
v-card-subtitle(v-html="$t('admin.default_images_help')")
v-card-text
v-row
v-col(cols='4')
@@ -109,12 +110,13 @@ import { mdiDeleteForever, mdiRestore, mdiPlus, mdiChevronUp } from '@mdi/js'
export default {
name: 'Theme',
data () {
const t = new Date().getMilliseconds()
return {
mdiDeleteForever, mdiRestore, mdiPlus, mdiChevronUp,
valid: false,
logoKey: 0,
fallbackImageKey: 0,
headerImageKey: 0,
logoKey: t,
fallbackImageKey: t,
headerImageKey: t,
link: { href: '', label: '' },
linkModal: false
// menu: [false, false, false, false]