Merge branch 'master' into gh
This commit is contained in:
commit
88e0c90a66
106 changed files with 7119 additions and 2881 deletions
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "gancio_plugins/gancio-plugin-telegram-bridge"]
|
||||
path = gancio_plugins/gancio-plugin-telegram-bridge
|
||||
url = https://framagit.org/les/gancio-plugin-telegram-bridge.git
|
35
CHANGELOG
35
CHANGELOG
|
@ -1,5 +1,40 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
|
||||
### 1.6.1 - 15 dec '22
|
||||
- allow edit tags in admin panel, fix #170
|
||||
- fix header / fallback image upload, fix #222
|
||||
- fix WPGancio MU
|
||||
- fix recurrent events label
|
||||
- update translations (de, es, eu, gl)
|
||||
|
||||
### 1.6.0 - 11 dec '22
|
||||
- new plugin system - fix #177
|
||||
- new "publish on telegram" plugin: (thanks @fadelkon)
|
||||
- people can now choose the language displayed - fix #171
|
||||
- admin could choose a custom fallback image - fix #195
|
||||
- it is now possible NOT to enter the end time of an event - fix #188
|
||||
- live search
|
||||
- improve event import
|
||||
- add Apple touch icon - fix #200
|
||||
- add nominatim / openstreetmap search feature (thanks @sedum)
|
||||
- new hide calendar option
|
||||
- new hide thumbs from homepage option
|
||||
- linkable admin tab
|
||||
- friendly instances label is now customizable (thanks @sedum)
|
||||
- i18n refactoring
|
||||
- Wordpress plugin now supports MU installation
|
||||
- new chinese translation
|
||||
- new portuguese translation
|
||||
- improved navbar layout
|
||||
- improved event layout
|
||||
- complete oauth2 refactoring
|
||||
- fix ics unique uuid
|
||||
- fix place "[Object]" issue - #194
|
||||
- fix random restart while downloading random media
|
||||
- fix mobile dialog layout
|
||||
- urlencode place and tag urls
|
||||
|
||||
|
||||
### 1.5.6 - 22 set '22
|
||||
- update linkifyjs, sequelizem, nuxt deps
|
||||
- improve homepage loading time
|
||||
|
|
|
@ -13,26 +13,50 @@ li {
|
|||
background-color: #434343;
|
||||
}
|
||||
|
||||
.v-application {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.v-application .p-description.text-body-1 {
|
||||
letter-spacing: normal !important;
|
||||
}
|
||||
|
||||
/* https://github.com/vuetifyjs/vuetify/issues/8875#issuecomment-559900683 */
|
||||
.v-slide-group__prev {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.v-toolbar .v-list-item__subtitle,
|
||||
.v-toolbar h4 {
|
||||
white-space: break-spaces;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
#home {
|
||||
max-width: 1400px;
|
||||
padding: 0px;
|
||||
overflow: hidden;
|
||||
/* padding: 0px; */
|
||||
/* overflow: hidden; */
|
||||
}
|
||||
|
||||
#events {
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#calh {
|
||||
@media only screen and (max-width: 600px) {
|
||||
#events {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.calh {
|
||||
/* this is to avoid content shift layout as v-calendar does not support SSR */
|
||||
height: 268px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.container {
|
||||
|
@ -44,8 +68,8 @@ li {
|
|||
max-width: 800px;
|
||||
}
|
||||
|
||||
.v-dialog .v-dialog--fullscreen {
|
||||
max-width: 100%;
|
||||
.v-dialog--fullscreen {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.v-application p {
|
||||
|
@ -81,8 +105,8 @@ li {
|
|||
width: 330px;
|
||||
max-width: 500px !important;
|
||||
flex-grow: 1;
|
||||
margin-top: .4em;
|
||||
margin-right: .4em;
|
||||
/* margin-top: 16px;
|
||||
margin-right: 16px; */
|
||||
transition: all .5s;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
@ -96,7 +120,7 @@ li {
|
|||
-webkit-box-orient: vertical;
|
||||
font-size: 1.1em !important;
|
||||
line-height: 1.2em !important;
|
||||
text-decoration: none;
|
||||
/* text-decoration: none; */
|
||||
}
|
||||
|
||||
.event .body {
|
||||
|
@ -113,6 +137,11 @@ li {
|
|||
text-decoration: none;
|
||||
}
|
||||
|
||||
.event a:hover,
|
||||
.event a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.vc-past {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
@ -121,12 +150,12 @@ li {
|
|||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.tags .v-chip .v-chip__content {
|
||||
/* .tags .v-chip .v-chip__content {
|
||||
max-width: 120px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
}
|
||||
} */
|
||||
|
||||
|
||||
.cursorPointer {
|
||||
|
|
50
components/Appbar.vue
Normal file
50
components/Appbar.vue
Normal file
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<nav>
|
||||
|
||||
<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>
|
||||
|
||||
<NavSearch />
|
||||
|
||||
<NavBar />
|
||||
|
||||
</nav>
|
||||
|
||||
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import NavHeader from './NavHeader.vue'
|
||||
import NavBar from './NavBar.vue'
|
||||
import NavSearch from './NavSearch.vue'
|
||||
|
||||
export default {
|
||||
name: 'Appbar',
|
||||
components: { NavHeader, NavBar, NavSearch },
|
||||
computed: mapState(['settings'])
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
nav {
|
||||
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(230,230,230,.95), rgba(250,250,250,.95)), url(/headerimage.png);
|
||||
}
|
||||
|
||||
#title {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -1,50 +1,64 @@
|
|||
<template lang="pug">
|
||||
#calendar
|
||||
vc-date-picker(
|
||||
v-model='selectedDate'
|
||||
title-position='left'
|
||||
:is-dark="settings['theme.is_dark']"
|
||||
:columns="$screens({ sm: 2 }, 1)"
|
||||
@input='click'
|
||||
@update:from-page='updatePage'
|
||||
:locale='$i18n.locale'
|
||||
:attributes='attributes'
|
||||
transition='fade'
|
||||
aria-label='Calendar'
|
||||
is-expanded
|
||||
is-inline)
|
||||
client-only
|
||||
vc-date-picker(
|
||||
ref='cal'
|
||||
v-model='selectedDate'
|
||||
title-position='left'
|
||||
:is-dark="settings['theme.is_dark']"
|
||||
:columns="!$vuetify.breakpoint.smAndDown ? 2 : 1"
|
||||
@input='click'
|
||||
@update:from-page='updatePage'
|
||||
:locale='$i18n.locale'
|
||||
:popover="{ visibility: 'click' }"
|
||||
:attributes='attributes'
|
||||
transition='fade'
|
||||
aria-label='Calendar'
|
||||
is-expanded
|
||||
is-inline)
|
||||
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)
|
||||
|
||||
</template>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import dayjs from 'dayjs'
|
||||
import { mdiChevronDown, mdiClose } from '@mdi/js'
|
||||
import { attributesFromEvents } from '../assets/helper'
|
||||
|
||||
export default {
|
||||
name: 'Calendar',
|
||||
props: {
|
||||
events: { type: Array, default: () => [] }
|
||||
},
|
||||
data () {
|
||||
const month = dayjs.tz().month() + 1
|
||||
const year = dayjs.tz().year()
|
||||
return {
|
||||
mdiChevronDown, mdiClose,
|
||||
selectedDate: null,
|
||||
page: { month, year }
|
||||
page: { month, year },
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['settings']),
|
||||
...mapState(['settings', 'events']),
|
||||
attributes () {
|
||||
return attributesFromEvents(this.events)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updatePage (page) {
|
||||
this.$emit('monthchange', page)
|
||||
if (page.month !== this.page.month || page.year !== this.page.year) {
|
||||
this.$root.$emit('monthchange', page)
|
||||
this.page.month = page.month
|
||||
this.page.year = page.year
|
||||
}
|
||||
},
|
||||
click (day) {
|
||||
this.$emit('dayclick', day)
|
||||
this.$root.$emit('dayclick', day)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,4 +72,9 @@ export default {
|
|||
.past-event {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
#calendarButton {
|
||||
margin-top: -6px;
|
||||
margin-left: -10px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -12,8 +12,8 @@ v-dialog(v-model='show'
|
|||
v-card-text(v-show='!!message') {{ message }}
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(text color='error' @click='cancel') {{$t('common.cancel')}}
|
||||
v-btn(text color='primary' @click='agree') {{$t('common.ok')}}
|
||||
v-btn(outlined color='error' @click='cancel') {{$t('common.cancel')}}
|
||||
v-btn(outlined color='primary' @click='agree') {{$t('common.ok')}}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -3,7 +3,7 @@ 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`) }}
|
||||
|
@ -15,16 +15,17 @@ v-col(cols=12)
|
|||
.datePicker.mt-3
|
||||
v-input(:value='fromDate' :rules="[$validators.required('common.when')]")
|
||||
vc-date-picker(
|
||||
v-model='fromDate'
|
||||
:value='fromDate'
|
||||
:is-range='type === "multidate"'
|
||||
@input="date => change('date', fromDate)"
|
||||
:timezone='settings.instance_timezone'
|
||||
@input="date => change('date', date)"
|
||||
:attributes='attributes'
|
||||
:locale='$i18n.locale'
|
||||
:is-dark="settings['theme.is_dark']"
|
||||
is-inline
|
||||
is-expanded
|
||||
:min-date='type !== "recurrent" && new Date()')
|
||||
template(#placeholder)
|
||||
span.calc Loading
|
||||
|
||||
div.text-center.mb-2(v-if='type === "recurrent"')
|
||||
span(v-if='value.recurrent.frequency !== "1m" && value.recurrent.frequency !== "2m"') {{ whenPatterns }}
|
||||
|
@ -37,7 +38,7 @@ v-col(cols=12)
|
|||
v-model="menuFromHour"
|
||||
:close-on-content-click="false"
|
||||
offset-y
|
||||
:value="fromHour"
|
||||
:value="value.fromHour"
|
||||
transition="scale-transition")
|
||||
template(v-slot:activator="{ on, attrs }")
|
||||
v-text-field(
|
||||
|
@ -45,7 +46,7 @@ v-col(cols=12)
|
|||
:clear-icon='mdiClose'
|
||||
@click:clear='() => change("fromHour")'
|
||||
:label="$t('event.from')"
|
||||
:value="fromHour"
|
||||
:value="value.fromHour"
|
||||
:disabled='!value.from'
|
||||
readonly
|
||||
:prepend-icon="mdiClockTimeFourOutline"
|
||||
|
@ -54,11 +55,11 @@ v-col(cols=12)
|
|||
v-on="on")
|
||||
v-time-picker(
|
||||
v-if="menuFromHour"
|
||||
v-model="fromHour"
|
||||
:value="value.fromHour"
|
||||
: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
|
||||
|
@ -66,7 +67,7 @@ v-col(cols=12)
|
|||
v-model="menuDueHour"
|
||||
:close-on-content-click="false"
|
||||
offset-y
|
||||
:value="dueHour"
|
||||
:value="value.dueHour"
|
||||
transition="scale-transition")
|
||||
template(v-slot:activator="{ on, attrs }")
|
||||
v-text-field(
|
||||
|
@ -74,19 +75,19 @@ v-col(cols=12)
|
|||
:clear-icon='mdiClose'
|
||||
@click:clear='() => change("dueHour")'
|
||||
:label="$t('event.due')"
|
||||
:value="dueHour"
|
||||
:disabled='!fromHour'
|
||||
:value="value.dueHour"
|
||||
:disabled='!value.fromHour'
|
||||
readonly
|
||||
:prepend-icon="mdiClockTimeEightOutline"
|
||||
v-bind="attrs"
|
||||
v-on="on")
|
||||
v-time-picker(
|
||||
v-if="menuDueHour"
|
||||
v-model="dueHour"
|
||||
:value="value.dueHour"
|
||||
: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")')
|
||||
|
||||
|
@ -102,25 +103,14 @@ export default {
|
|||
name: 'DateInput',
|
||||
components: { List },
|
||||
props: {
|
||||
value: { type: Object, default: () => ({ from: null, due: null, recurrent: null }) },
|
||||
value: { type: Object, default: () => ({ from: null, due: null, recurrent: null, fromHour: null, dueHour: null }) },
|
||||
event: { type: Object, default: () => null }
|
||||
},
|
||||
data() {
|
||||
let fromDate
|
||||
if (this.value.from) {
|
||||
if (this.value.multidate) {
|
||||
fromDate = ({ start: dayjs(this.value.from).toDate(), end: dayjs(this.value.due).toDate() })
|
||||
} else {
|
||||
fromDate = new Date(this.value.from)
|
||||
}
|
||||
}
|
||||
return {
|
||||
mdiClockTimeFourOutline, mdiClockTimeEightOutline, mdiClose,
|
||||
allowedMinutes: [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55],
|
||||
menuFromHour: false,
|
||||
fromDate,
|
||||
fromHour: this.value.from && dayjs.tz(this.value.from).format('HH:mm'),
|
||||
dueHour: this.value.due && dayjs.tz(this.value.due).format('HH:mm'),
|
||||
menuDueHour: false,
|
||||
type: this.value.type || 'normal',
|
||||
events: [],
|
||||
|
@ -133,6 +123,15 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapState(['settings']),
|
||||
fromDate () {
|
||||
if (this.value.from) {
|
||||
if (this.value.multidate) {
|
||||
return ({ start: dayjs(this.value.from).toDate(), end: dayjs(this.value.due).toDate() })
|
||||
} else {
|
||||
return new Date(this.value.from)
|
||||
}
|
||||
}
|
||||
},
|
||||
todayEvents() {
|
||||
const start = dayjs.tz(this.value.from).startOf('day').unix()
|
||||
const end = dayjs.tz(this.value.from).endOf('day').unix()
|
||||
|
@ -227,60 +226,53 @@ export default {
|
|||
} else if (what === 'recurrentType') {
|
||||
this.$emit('input', { ...this.value, recurrent: { ...this.value.recurrent, type: value } })
|
||||
} else if (what === 'fromHour') {
|
||||
// if (value) {
|
||||
const [hour, minute] = value ? value.split(':') : [0, 0]
|
||||
let from = dayjs.tz(this.value.from).hour(hour).minute(minute).second(0).toDate()
|
||||
this.$emit('input', { ...this.value, from })
|
||||
if (!value) {
|
||||
this.fromHour = null
|
||||
if (value) {
|
||||
this.$emit('input', { ...this.value, fromHour: value })
|
||||
} else {
|
||||
this.$emit('input', { ...this.value, fromHour: null, dueHour: null })
|
||||
}
|
||||
// } else {
|
||||
// this.$emit('input', { ...this.value })
|
||||
// }
|
||||
} else if (what === 'dueHour') {
|
||||
if (value) {
|
||||
this.value.due = this.value.due ? this.value.due : this.value.from
|
||||
const [hour, minute] = value.split(':')
|
||||
let due = dayjs.tz(this.value.due || this.value.from).hour(Number(hour)).minute(Number(minute)).second(0)
|
||||
|
||||
// add a day
|
||||
if (dayjs(this.value.from).hour() > Number(hour) && !this.value.multidate) {
|
||||
due = due.add(1, 'day')
|
||||
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()
|
||||
}
|
||||
}
|
||||
due = due.hour(hour).minute(minute).second(0)
|
||||
this.$emit('input', { ...this.value, due: due.toDate() })
|
||||
} else {
|
||||
this.$emit('input', { ...this.value, due: null })
|
||||
this.dueHour = null
|
||||
this.value.due = null
|
||||
}
|
||||
this.$emit('input', { ...this.value, dueHour: value })
|
||||
|
||||
// if (value) {
|
||||
// // const [hour, minute] = value.split(':')
|
||||
// // let due = dayjs.tz(this.value.due || this.value.from).hour(Number(hour)).minute(Number(minute)).second(0)
|
||||
|
||||
// // add a day
|
||||
// // if (dayjs(this.value.from).hour() > Number(hour) && !this.value.multidate) {
|
||||
// // due = due.add(1, 'day')
|
||||
// // }
|
||||
// // due = due.hour(hour).minute(minute).second(0)
|
||||
// } else {
|
||||
// this.$emit('input', { ...this.value, dueHour: null })
|
||||
// }
|
||||
// change date in calendar (could be a range or a recurrent event...)
|
||||
} else if (what === 'date') {
|
||||
if (value === null) {
|
||||
this.$emit('input', { ...this.value, from: null, due: null })
|
||||
this.$emit('input', { ...this.value, from: null, due: null, fromHour: null, dueHour: null })
|
||||
return
|
||||
}
|
||||
if (this.value.multidate) {
|
||||
let from = value.start
|
||||
let due = value.end
|
||||
if (this.fromHour) {
|
||||
const [hour, minute] = this.fromHour.split(':')
|
||||
from = dayjs.tz(from).hour(hour).minute(minute).second(0).toDate()
|
||||
}
|
||||
if (this.dueHour) {
|
||||
const [hour, minute] = this.dueHour.split(':')
|
||||
due = dayjs.tz(due).hour(hour).minute(minute).second(0).toDate()
|
||||
}
|
||||
this.$emit('input', { ...this.value, from, due })
|
||||
} else {
|
||||
let from = value
|
||||
let due = this.value.due
|
||||
if (this.fromHour) {
|
||||
const [hour, minute] = this.fromHour.split(':')
|
||||
from = dayjs.tz(value).hour(hour).minute(minute).second(0).toDate()
|
||||
}
|
||||
if (this.dueHour) {
|
||||
const [hour, minute] = this.dueHour.split(':')
|
||||
due = dayjs.tz(value).hour(hour).minute(minute).second(0).toDate()
|
||||
}
|
||||
this.$emit('input', { ...this.value, from, due })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -224,6 +224,7 @@ export default {
|
|||
transition: opacity .5s;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
overflow: scroll;
|
||||
// position: absolute;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
<template lang="pug">
|
||||
v-card.h-event.event.d-flex(itemscope itemtype="https://schema.org/Event")
|
||||
nuxt-link(:to='`/event/${event.slug || event.id}`' itemprop="url")
|
||||
MyPicture(:event='event' thumb :lazy='lazy')
|
||||
MyPicture(v-if='!settings.hide_thumbs' :event='event' thumb :lazy='lazy')
|
||||
v-icon.float-right.mr-1(v-if='event.parentId' color='success' v-text='mdiRepeat')
|
||||
.title.p-name(itemprop="name") {{ event.title }}
|
||||
|
||||
v-card-text.body.pt-0.pb-0
|
||||
time.dt-start.subtitle-1(:datetime='event.start_datetime | unixFormat("YYYY-MM-DD HH:mm")' itemprop="startDate" :content="event.start_datetime | unixFormat('YYYY-MM-DDTHH:mm')") <v-icon v-text='mdiCalendar'></v-icon> {{ event | when }}
|
||||
.d-none.dt-end(itemprop="endDate" :content="event.end_datetime | unixFormat('YYYY-MM-DDTHH:mm')") {{ event.end_datetime | unixFormat('YYYY-MM-DD HH:mm') }}
|
||||
nuxt-link.place.d-block.p-location.pl-0(text color='primary' :to='`/place/${event.place.name}`' itemprop="location" itemscope itemtype="https://schema.org/Place") <v-icon v-text='mdiMapMarker'></v-icon> <span itemprop='name'>{{ event.place.name }}</span>
|
||||
.d-none.dt-end(v-if='event.end_datetime' itemprop="endDate" :content="event.end_datetime | unixFormat('YYYY-MM-DDTHH:mm')") {{ event.end_datetime | unixFormat('YYYY-MM-DD HH:mm') }}
|
||||
nuxt-link.place.d-block.p-location.pl-0(text :to='`/place/${encodeURIComponent(event.place.name)}`' itemprop="location" itemscope itemtype="https://schema.org/Place") <v-icon v-text='mdiMapMarker'></v-icon> <span itemprop='name'>{{ event.place.name }}</span>
|
||||
.d-none(itemprop='address') {{ event.place.address }}
|
||||
|
||||
v-card-actions.pt-0.actions.justify-space-between
|
||||
.tags
|
||||
v-chip.ml-1.mt-1(v-for='tag in event.tags.slice(0, 6)' small :to='`/tag/${tag}`'
|
||||
:key='tag' outlined color='primary') {{ tag }}
|
||||
v-card-actions.flex-wrap
|
||||
v-chip.ml-1.mt-1(v-for='tag in event.tags.slice(0, 6)' small label :to='`/tag/${encodeURIComponent(tag)}`'
|
||||
:key='tag' outlined color='primary') {{ tag }}
|
||||
|
||||
</template>
|
||||
<script>
|
||||
|
|
|
@ -15,8 +15,8 @@ v-card
|
|||
|
||||
v-card-actions(v-if='isDialog')
|
||||
v-spacer
|
||||
v-btn(v-if='isDialog' color='warning' @click="$emit('close')") {{$t("common.cancel")}}
|
||||
v-btn(:disabled='(!couldGo || !proceed)' :href='link' target='_blank'
|
||||
v-btn(v-if='isDialog' outlined color='warning' @click="$emit('close')") {{$t("common.cancel")}}
|
||||
v-btn(:disabled='(!couldGo || !proceed)' outlined :href='link' target='_blank'
|
||||
:loading='loading' color="primary") {{$t("common.follow")}}
|
||||
</template>
|
||||
<script>
|
||||
|
|
|
@ -4,7 +4,6 @@ v-footer(aria-label='Footer')
|
|||
v-dialog(v-model='showFollowMe' destroy-on-close max-width='700px' :fullscreen='$vuetify.breakpoint.xsOnly')
|
||||
FollowMe(@close='showFollowMe=false' is-dialog)
|
||||
|
||||
v-btn(color='primary' text href='https://gancio.org' target='_blank' rel="noopener") Gancio <small>{{settings.version}}</small>
|
||||
v-btn.ml-1(v-for='link in footerLinks'
|
||||
:key='link.label' color='primary' text
|
||||
:href='link.href' :to='link.to' :target="link.href && '_blank'") {{link.label}}
|
||||
|
@ -12,7 +11,7 @@ v-footer(aria-label='Footer')
|
|||
v-menu(v-if='settings.enable_trusted_instances && settings.trusted_instances && settings.trusted_instances.length'
|
||||
offset-y bottom open-on-hover transition="slide-y-transition")
|
||||
template(v-slot:activator="{ on, attrs }")
|
||||
v-btn.ml-1(v-bind='attrs' v-on='on' color='primary' text) {{$t('common.places')}}
|
||||
v-btn.ml-1(v-bind='attrs' v-on='on' color='primary' text) {{ settings.trusted_instances_label || $t('admin.trusted_instances_label_default')}}
|
||||
v-list(subheaders two-lines)
|
||||
v-list-item(v-for='instance in settings.trusted_instances'
|
||||
:key='instance.name'
|
||||
|
@ -26,6 +25,9 @@ v-footer(aria-label='Footer')
|
|||
v-list-item-subtitle {{instance.label}}
|
||||
|
||||
v-btn.ml-1(v-if='settings.enable_federation' color='primary' text rel='me' @click.prevent='showFollowMe=true') {{$t('event.interact_with_me')}}
|
||||
v-spacer
|
||||
v-btn(color='primary' text href='https://gancio.org' target='_blank' rel="noopener") Gancio <small>{{settings.version}}</small>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
@ -44,9 +46,9 @@ export default {
|
|||
if (!this.settings || !this.settings.footerLinks) return []
|
||||
return this.settings.footerLinks.map(link => {
|
||||
if (/^https?:\/\//.test(link.href)) {
|
||||
return { href: link.href, label: link.label }
|
||||
return { href: link.href, label: link.label.startsWith('common.') ? this.$t(link.label) : link.label }
|
||||
} else {
|
||||
return { to: link.href, label: link.label }
|
||||
return { to: link.href, label: link.label.startsWith('common.') ? this.$t(link.label) : link.label }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ v-card
|
|||
:error-messages='errorMessage')
|
||||
.col
|
||||
v-file-input(
|
||||
:prepend-icon='mdiAttachment'
|
||||
v-model='file'
|
||||
accept=".ics"
|
||||
:label="$t('event.ics')"
|
||||
|
@ -22,12 +23,13 @@ v-card
|
|||
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(text @click='$emit("close")' color='warning') {{$t('common.cancel')}}
|
||||
v-btn(text @click='importGeneric' :loading='loading' :disabled='loading'
|
||||
v-btn(outlined @click='$emit("close")' color='warning') {{$t('common.cancel')}}
|
||||
v-btn(outlined @click='importGeneric' :loading='loading' :disabled='loading'
|
||||
color='primary') {{$t('common.import')}}
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import { mdiAttachment } from '@mdi/js'
|
||||
import ical from 'ical.js'
|
||||
import get from 'lodash/get'
|
||||
|
||||
|
@ -35,6 +37,7 @@ export default {
|
|||
name: 'ImportDialog',
|
||||
data () {
|
||||
return {
|
||||
mdiAttachment,
|
||||
file: null,
|
||||
errorMessage: '',
|
||||
error: false,
|
||||
|
|
|
@ -23,7 +23,7 @@ export default {
|
|||
.loading-page {
|
||||
z-index: -10;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
top: 178px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
115
components/Map.vue
Normal file
115
components/Map.vue
Normal file
|
@ -0,0 +1,115 @@
|
|||
<template lang="pug">
|
||||
client-only(placeholder='Loading...' )
|
||||
v-card
|
||||
v-container
|
||||
LMap(ref="map"
|
||||
id="leaflet-map"
|
||||
:zoom="zoom"
|
||||
:options="{attributionControl: false}"
|
||||
:center="center")
|
||||
LControlAttribution(position='bottomright' prefix="")
|
||||
LTileLayer(
|
||||
:url="url"
|
||||
:attribution="attribution")
|
||||
LMarker(
|
||||
:lat-lng="marker.coordinates")
|
||||
|
||||
v-row.my-4.d-flex.flex-column.align-center.text-center
|
||||
.text-h6
|
||||
v-icon(v-text='mdiMapMarker' )
|
||||
nuxt-link.ml-2.text-decoration-none(v-text="event.place.name" :to='`/place/${event.place.name}`')
|
||||
.mx-2(v-text="`${event.place.address}`")
|
||||
v-card-actions
|
||||
v-row
|
||||
//- p.my-4(v-text="$t('common.getting_there')")
|
||||
v-btn.ml-2(icon large :href="routeBy('foot')")
|
||||
v-icon(v-text='mdiWalk')
|
||||
v-btn.ml-2(icon large :href="routeBy('bike')")
|
||||
v-icon(v-text='mdiBike')
|
||||
v-btn.ml-2(icon large :href="routeBy('car')")
|
||||
v-icon(v-text='mdiCar')
|
||||
v-spacer
|
||||
v-btn(@click='$emit("close")' outlined) Close
|
||||
</template>
|
||||
<script>
|
||||
|
||||
import "leaflet/dist/leaflet.css"
|
||||
import { LMap, LTileLayer, LMarker, LPopup, LControlAttribution } from 'vue2-leaflet'
|
||||
import { mapActions, mapState } from 'vuex'
|
||||
import { Icon } from 'leaflet'
|
||||
import { mdiWalk, mdiBike, mdiCar, mdiMapMarker } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LMap,
|
||||
LTileLayer,
|
||||
LMarker,
|
||||
LPopup,
|
||||
LControlAttribution
|
||||
},
|
||||
data ({ $store }) {
|
||||
return {
|
||||
mdiWalk, mdiBike, mdiCar, mdiMapMarker,
|
||||
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: {
|
||||
address: this.event.place.address,
|
||||
coordinates: {lat: this.event.place.latitude, lon: this.event.place.longitude}
|
||||
},
|
||||
routingProvider: 'openstreetmap',
|
||||
}
|
||||
},
|
||||
props: {
|
||||
event: { type: Object, default: () => ({}) }
|
||||
},
|
||||
mounted() {
|
||||
delete Icon.Default.prototype._getIconUrl;
|
||||
Icon.Default.mergeOptions({
|
||||
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
|
||||
iconUrl: require('leaflet/dist/images/marker-icon.png'),
|
||||
shadowUrl: require('leaflet/dist/images/marker-shadow.png'),
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.$refs.map.mapObject.invalidateSize();
|
||||
}, 200);
|
||||
},
|
||||
computed: {
|
||||
...mapState(['settings']),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setSetting']),
|
||||
// mountLocateControl() {
|
||||
// this.$refs.map.mapObject.locate({
|
||||
// locateOptions: {
|
||||
// maxZoom: 10
|
||||
// }
|
||||
// });
|
||||
// this.$refs.map.mapObject.MyLocate();
|
||||
// },
|
||||
routeBy (type) {
|
||||
const lat = this.event.place.latitude
|
||||
const lon = this.event.place.longitude
|
||||
const routingType = {
|
||||
foot: "engine=fossgis_osrm_foot",
|
||||
bike: "engine=fossgis_osrm_bike",
|
||||
transit: null,
|
||||
car: "engine=fossgis_osrm_car"
|
||||
}
|
||||
return `https://www.openstreetmap.org/directions?from=&to=${lat},${lon}&${routingType[type]}#map=14/${lat}/${lon}`
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#leaflet-map {
|
||||
height: 55vh;
|
||||
width: 100%;
|
||||
border-radius: .3rem;
|
||||
border: 1px solid #fff;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
|
@ -10,7 +10,7 @@
|
|||
:height="height" :width="width"
|
||||
:style="{ 'object-position': thumbnailPosition }">
|
||||
|
||||
<img v-else-if='!media && thumb' class='thumb' src="/noimg.svg" alt=''>
|
||||
<img v-else-if='!media && thumb' class='thumb' src="/fallbackimage.png" alt=''>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
<template lang="pug">
|
||||
v-app-bar(app aria-label='Menu' height=64)
|
||||
|
||||
//- logo, title and description
|
||||
v-list-item(:to='$route.name==="index"?"/about":"/"')
|
||||
v-list-item-avatar.ma-xs-1(tile)
|
||||
img(src='/logo.png' height='40')
|
||||
v-list-item-content
|
||||
v-list-item-title.d-flex
|
||||
h2 {{settings.title}}
|
||||
v-list-item-subtitle.d-none.d-sm-flex {{settings.description}}
|
||||
|
||||
v-spacer
|
||||
v-btn(v-if='loggedIn || settings.allow_anon_event' icon nuxt to='/add' :aria-label='$t("common.add_event")' :title='$t("common.add_event")')
|
||||
v-icon(large color='primary' v-text='mdiPlus')
|
||||
|
||||
v-btn(icon nuxt to='/export' :title='$t("common.share")' :aria-label='$t("common.share")')
|
||||
v-icon(v-text='mdiShareVariant')
|
||||
|
||||
v-btn(v-if='!loggedIn' icon nuxt to='/login' :title='$t("common.login")' :aria-label='$t("common.login")')
|
||||
v-icon(v-text='mdiLogin')
|
||||
|
||||
client-only
|
||||
v-menu(v-if='loggedIn' offset-y eager)
|
||||
template(v-slot:activator="{ on, attrs }")
|
||||
v-btn(icon v-bind='attrs' v-on='on' title='Menu' aria-label='Menu')
|
||||
v-icon(v-text='mdiDotsVertical')
|
||||
v-list
|
||||
v-list-item(nuxt to='/settings')
|
||||
v-list-item-icon
|
||||
v-icon(v-text='mdiCog')
|
||||
v-list-item-content
|
||||
v-list-item-title {{$t('common.settings')}}
|
||||
|
||||
v-list-item(v-if='$auth.user.is_admin' nuxt to='/admin')
|
||||
v-list-item-icon
|
||||
v-icon(v-text='mdiAccount')
|
||||
v-list-item-content
|
||||
v-list-item-title {{$t('common.admin')}}
|
||||
|
||||
v-list-item(@click='logout')
|
||||
v-list-item-icon
|
||||
v-icon(v-text='mdiLogout')
|
||||
v-list-item-content
|
||||
v-list-item-title {{$t('common.logout')}}
|
||||
template(#placeholder)
|
||||
v-btn(v-if='loggedIn' icon aria-label='Menu' title='Menu')
|
||||
v-icon(v-text='mdiDotsVertical')
|
||||
|
||||
|
||||
v-btn(icon target='_blank' :href='`${settings.baseurl}/feed/rss`' title='RSS' aria-label='RSS')
|
||||
v-icon(color='orange' v-text='mdiRss')
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import clipboard from '../assets/clipboard'
|
||||
import { mdiPlus, mdiShareVariant, mdiLogin, mdiDotsVertical, mdiLogout, mdiAccount, mdiCog, mdiRss } from '@mdi/js'
|
||||
|
||||
|
||||
export default {
|
||||
name: 'Nav',
|
||||
data () {
|
||||
return { mdiPlus, mdiShareVariant, mdiLogout, mdiLogin, mdiDotsVertical, mdiAccount, mdiCog, mdiRss }
|
||||
},
|
||||
mixins: [clipboard],
|
||||
computed: {
|
||||
loggedIn () {
|
||||
return this.$auth.loggedIn
|
||||
},
|
||||
...mapState(['settings']),
|
||||
},
|
||||
methods: {
|
||||
logout () {
|
||||
this.$root.$message('common.logout_ok')
|
||||
this.$auth.logout()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
29
components/NavBar.vue
Normal file
29
components/NavBar.vue
Normal file
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<v-tabs centered background-color='transparent' optional dense icons-and-text class='mt-4'>
|
||||
<v-tab to='/'>
|
||||
<span class='d-none d-sm-flex'>Home</span>
|
||||
<v-icon v-text='mdiHome' />
|
||||
</v-tab>
|
||||
<v-tab v-if='$auth.loggedIn || settings.allow_anon_event' to='/add'>
|
||||
<span class='d-none d-sm-flex'>{{$t('common.add_event')}}</span>
|
||||
<v-icon color='primary' v-text='mdiPlus' />
|
||||
</v-tab>
|
||||
<v-tab to='/export' >
|
||||
<span class='d-none d-sm-flex'>{{$t('common.share')}}</span>
|
||||
<v-icon v-text='mdiShareVariant' />
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mdiPlus, mdiShareVariant, mdiHome, mdiInformation } from '@mdi/js'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'Navbar',
|
||||
data () {
|
||||
return { mdiPlus, mdiShareVariant, mdiHome, mdiInformation }
|
||||
},
|
||||
computed: mapState(['settings'])
|
||||
}
|
||||
</script>
|
99
components/NavHeader.vue
Normal file
99
components/NavHeader.vue
Normal file
|
@ -0,0 +1,99 @@
|
|||
<template>
|
||||
<div class='d-flex pa-4'>
|
||||
<v-btn icon large nuxt to='/'>
|
||||
<img src='/logo.png' height='40' />
|
||||
</v-btn>
|
||||
|
||||
<v-spacer/>
|
||||
|
||||
<div class='d-flex'>
|
||||
<v-btn icon large to='/about' :title='$t("common.about")' :aria-label='$t("common.about")'>
|
||||
<v-icon v-text='mdiInformation' />
|
||||
</v-btn>
|
||||
|
||||
<client-only>
|
||||
<v-menu offset-y transition="slide-y-transition">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn icon large v-bind='attrs' v-on='on' aria-label='Language' v-text="$i18n.locale" />
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item v-for='locale in $i18n.locales' @click.prevent.stop="$i18n.setLocale(locale.code)" :key='locale.code'>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text='locale.name' />
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item nuxt target='_blank' href='https://hosted.weblate.org/engage/gancio/'>
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle v-text='$t("common.help_translate")' />
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-btn slot='placeholder' large icon arial-label='Language'>{{$i18n.locale}}</v-btn>
|
||||
</client-only>
|
||||
|
||||
<client-only>
|
||||
<v-menu v-if='$auth.loggedIn' offset-y transition="slide-y-transition">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn class='mr-0' large icon v-bind='attrs' v-on='on' title='Menu' aria-label='Menu'>
|
||||
<v-icon v-text='mdiDotsVertical' />
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item nuxt to='/settings'>
|
||||
<v-list-item-icon><v-icon v-text='mdiCog'></v-icon></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="$t('common.settings')"/>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if='$auth.user.is_admin' nuxt to='/admin'>
|
||||
<v-list-item-icon>
|
||||
<v-icon v-text='mdiAccount' />
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="$t('common.admin')" />
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click='logout'>
|
||||
<v-list-item-icon>
|
||||
<v-icon v-text='mdiLogout' />
|
||||
</v-list-item-icon>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title v-text="$t('common.logout')" />
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<template #placeholder>
|
||||
<v-btn v-if='$auth.loggedIn' large icon aria-label='Menu' title='Menu'>
|
||||
<v-icon v-text='mdiDotsVertical' />
|
||||
</v-btn>
|
||||
</template>
|
||||
</client-only>
|
||||
|
||||
<!-- login button -->
|
||||
<v-btn class='mr-0' v-if='!$auth.loggedIn' large icon nuxt to='/login' :title='$t("common.login")' :aria-label='$t("common.login")'>
|
||||
<v-icon v-text='mdiLogin' />
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
import { mdiLogin, mdiDotsVertical, mdiLogout, mdiAccount, mdiCog, mdiInformation } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return { mdiLogin, mdiDotsVertical, mdiLogout, mdiAccount, mdiCog, mdiInformation }
|
||||
},
|
||||
methods: {
|
||||
logout () {
|
||||
this.$root.$message('common.logout_ok')
|
||||
this.$auth.logout()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
43
components/NavSearch.vue
Normal file
43
components/NavSearch.vue
Normal file
|
@ -0,0 +1,43 @@
|
|||
<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}}
|
||||
</template>
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import Calendar from '@/components/Calendar'
|
||||
import { mdiMagnify, mdiClose } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
mdiMagnify, mdiClose,
|
||||
collections: []
|
||||
}),
|
||||
async fetch () {
|
||||
this.collections = await this.$axios.$get('collections').catch(_e => [])
|
||||
},
|
||||
components: { Calendar },
|
||||
computed: {
|
||||
showSearchBar () {
|
||||
return this.$route.name === 'index'
|
||||
},
|
||||
showCollectionsBar () {
|
||||
return ['index', 'collection-collection'].includes(this.$route.name)
|
||||
},
|
||||
...mapState(['settings'])
|
||||
},
|
||||
methods: {
|
||||
search (ev) {
|
||||
this.$root.$emit('search', ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#navsearch {
|
||||
margin: 0 auto;
|
||||
max-width: 800px;
|
||||
}
|
||||
</style>
|
|
@ -6,6 +6,7 @@ v-snackbar(
|
|||
:top="top"
|
||||
:left="left"
|
||||
:right="right"
|
||||
transition='scroll-x-reverse-transition'
|
||||
:timeout="timeout")
|
||||
v-icon.mr-3(color="white" v-text='icon')
|
||||
span {{ message }}
|
||||
|
@ -25,7 +26,7 @@ export default {
|
|||
bottom: true,
|
||||
top: false,
|
||||
left: false,
|
||||
right: false,
|
||||
right: true,
|
||||
active: false,
|
||||
timeout: 5000,
|
||||
message: ''
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template lang="pug">
|
||||
v-row
|
||||
v-row.mb-4
|
||||
v-col(cols=12 md=6)
|
||||
v-combobox(ref='place'
|
||||
:rules="[$validators.required('common.where')]"
|
||||
|
@ -7,12 +7,12 @@ v-row
|
|||
:hint="$t('event.where_description')"
|
||||
:prepend-icon='mdiMapMarker'
|
||||
no-filter
|
||||
:value='value.name'
|
||||
hide-no-data
|
||||
@input.native='search'
|
||||
persistent-hint
|
||||
:value='value.name'
|
||||
item-text='name'
|
||||
:items="places"
|
||||
@focus='search'
|
||||
@change='selectPlace')
|
||||
template(v-slot:item="{ item, attrs, on }")
|
||||
v-list-item(v-bind='attrs' v-on='on')
|
||||
|
@ -22,35 +22,90 @@ v-row
|
|||
v-list-item-title(v-text='item.name')
|
||||
v-list-item-subtitle(v-text='item.address')
|
||||
|
||||
|
||||
v-col(cols=12 md=6)
|
||||
v-text-field(ref='address'
|
||||
:prepend-icon='mdiMap'
|
||||
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'
|
||||
:rules="[ v => disableAddress ? true : $validators.required('common.address')(v)]"
|
||||
@input.native='searchAddress'
|
||||
:label="$t('common.address')"
|
||||
@change="changeAddress"
|
||||
:value="value.address")
|
||||
:rules="[ v => disableAddress ? true : $validators.required('common.address')(v)]"
|
||||
:value='value.address'
|
||||
item-text='address'
|
||||
persistent-hint hide-no-data clearable no-filter
|
||||
:loading='loading'
|
||||
@change='selectAddress'
|
||||
@focus='searchAddress'
|
||||
:items="addressList"
|
||||
:hint="$t('event.address_description_osm')")
|
||||
template(v-slot:message="{message, 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)')
|
||||
v-list-item-content(two-line v-if='item')
|
||||
v-list-item-title(v-text='item.name')
|
||||
v-list-item-subtitle(v-text='`${item.address}`')
|
||||
//- v-col(cols=12 md=3 v-if='settings.allow_geolocation')
|
||||
//- v-text-field(ref='latitude' :value='value.latitude'
|
||||
//- :prepend-icon='mdiLatitude'
|
||||
//- :disabled='disableDetails'
|
||||
//- :label="$t('common.latitude')" )
|
||||
//- v-col(cols=12 md=3 v-if='settings.allow_geolocation')
|
||||
//- v-text-field(ref='longitude' :value='value.longitude'
|
||||
//- :prepend-icon='mdiLongitude'
|
||||
//- :disabled='disableDetails'
|
||||
//- :label="$t('common.longitude')")
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import { mdiMap, mdiMapMarker, mdiPlus } from '@mdi/js'
|
||||
import { mdiMap, mdiMapMarker, mdiPlus, mdiMapSearch, mdiLatitude, mdiLongitude, mdiRoadVariant, mdiHome, mdiCityVariant } from '@mdi/js'
|
||||
import { mapState } from 'vuex'
|
||||
import debounce from 'lodash/debounce'
|
||||
import get from 'lodash/get'
|
||||
|
||||
export default {
|
||||
name: 'WhereInput',
|
||||
props: {
|
||||
value: { type: Object, default: () => ({}) }
|
||||
},
|
||||
data () {
|
||||
data ( {$store} ) {
|
||||
return {
|
||||
mdiMap, mdiMapMarker, mdiPlus,
|
||||
mdiMap, mdiMapMarker, mdiPlus, mdiMapSearch, mdiLatitude, mdiLongitude, mdiRoadVariant, mdiHome, mdiCityVariant,
|
||||
place: { },
|
||||
placeName: '',
|
||||
places: [],
|
||||
disableAddress: true
|
||||
disableAddress: true,
|
||||
addressList: [],
|
||||
loading: false,
|
||||
nominatim_osm_type: {
|
||||
way: mdiRoadVariant,
|
||||
house: mdiHome,
|
||||
node: mdiMapMarker,
|
||||
relation: mdiCityVariant,
|
||||
},
|
||||
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: {
|
||||
...mapState(['settings']),
|
||||
filteredPlaces () {
|
||||
if (!this.placeName) { return this.places }
|
||||
const placeName = this.placeName.trim().toLowerCase()
|
||||
|
@ -80,16 +135,34 @@ export default {
|
|||
this.places.unshift({ create: true, name: ev.target.value.trim() })
|
||||
}
|
||||
}, 100),
|
||||
loadCoordinatesResultIcon(item) {
|
||||
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]
|
||||
}
|
||||
},
|
||||
selectPlace (p) {
|
||||
if (!p) { return }
|
||||
if (typeof p === 'object' && !p.create) {
|
||||
this.place.name = p.name.trim()
|
||||
if (p.id === this.value.id) return
|
||||
this.place.name = p.name
|
||||
this.place.address = p.address
|
||||
if (this.settings.allow_geolocation) {
|
||||
this.place.latitude = p.latitude
|
||||
this.place.longitude = p.longitude
|
||||
}
|
||||
this.place.id = p.id
|
||||
this.disableAddress = true
|
||||
} else { // this is a new place
|
||||
this.place.name = p.name || p
|
||||
const tmpPlace = this.place.name.trim().toLocaleLowerCase()
|
||||
this.place.name = (p.name || p).trim()
|
||||
const tmpPlace = this.place.name.toLocaleLowerCase()
|
||||
// search for a place with the same name
|
||||
const place = this.places.find(p => !p.create && p.name.trim().toLocaleLowerCase() === tmpPlace)
|
||||
if (place) {
|
||||
|
@ -100,6 +173,11 @@ export default {
|
|||
} else {
|
||||
delete this.place.id
|
||||
this.place.address = ''
|
||||
if (this.settings.allow_geolocation) {
|
||||
this.place.details = p.details
|
||||
this.place.latitude = p.latitude
|
||||
this.place.longitude = p.longitude
|
||||
}
|
||||
this.disableAddress = false
|
||||
this.$refs.place.blur()
|
||||
this.$refs.address.focus()
|
||||
|
@ -110,7 +188,108 @@ export default {
|
|||
changeAddress (v) {
|
||||
this.place.address = v
|
||||
this.$emit('input', { ...this.place })
|
||||
}
|
||||
this.disableDetails = false
|
||||
},
|
||||
selectAddress (v) {
|
||||
if (!v) { return }
|
||||
if (typeof v === 'object') {
|
||||
this.place.latitude = v.lat
|
||||
this.place.longitude = v.lon
|
||||
this.place.address = v.address
|
||||
// }
|
||||
} else {
|
||||
this.place.address = v
|
||||
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
|
||||
const searchCoordinates = pre_searchCoordinates.replace('/', ',')
|
||||
// const regex_coords_comma = "-?[1-9][0-9]*(\\.[0-9]+)?,\\s*-?[1-9][0-9]*(\\.[0-9]+)?";
|
||||
// const regex_coords_slash = "-?[1-9][0-9]*(\\.[0-9]+)?/\\s*-?[1-9][0-9]*(\\.[0-9]+)?";
|
||||
|
||||
// const setCoords = (v) => {
|
||||
// const lat = v[0].trim()
|
||||
// const lon = v[1].trim()
|
||||
// // check coordinates are valid
|
||||
// if ((lat < 90 && lat > -90)
|
||||
// && (lon < 180 && lon > -180)) {
|
||||
// this.place.latitude = lat
|
||||
// this.place.longitude = lon
|
||||
// } else {
|
||||
// this.$root.$message("Non existent coordinates", { color: 'error' })
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (pre_searchCoordinates.match(regex_coords_comma)) {
|
||||
// let v = pre_searchCoordinates.split(",")
|
||||
// setCoords(v)
|
||||
// return
|
||||
// }
|
||||
// if (pre_searchCoordinates.match(regex_coords_slash)) {
|
||||
// let v = pre_searchCoordinates.split("/")
|
||||
// setCoords(v)
|
||||
// return
|
||||
// }
|
||||
|
||||
if (searchCoordinates.length) {
|
||||
this.loading = true
|
||||
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
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -22,6 +22,7 @@ v-container
|
|||
v-data-table(
|
||||
v-if='announcements.length'
|
||||
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
|
||||
:header-props='{ sortIcon: mdiChevronDown }'
|
||||
:headers='headers'
|
||||
:items='announcements')
|
||||
template(v-slot:item.actions='{ item }')
|
||||
|
@ -35,21 +36,21 @@ import { mapActions } from 'vuex'
|
|||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import Editor from '../Editor'
|
||||
import Announcement from '../Announcement'
|
||||
import { mdiPlus, mdiChevronRight, mdiChevronLeft } from '@mdi/js'
|
||||
import { mdiPlus, mdiChevronRight, mdiChevronLeft, mdiChevronDown } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
components: { Editor, Announcement },
|
||||
data() {
|
||||
return {
|
||||
mdiPlus, mdiChevronRight, mdiChevronLeft,
|
||||
mdiPlus, mdiChevronRight, mdiChevronLeft, mdiChevronDown,
|
||||
valid: false,
|
||||
dialog: false,
|
||||
editing: false,
|
||||
announcements: [],
|
||||
loading: false,
|
||||
headers: [
|
||||
{ value: 'title', text: 'Title' },
|
||||
{ value: 'actions', text: 'Actions', align: 'right' }
|
||||
{ value: 'title', text: this.$t('common.title') },
|
||||
{ value: 'actions', text: this.$t('common.actions'), align: 'right' }
|
||||
],
|
||||
announcement: { title: '', announcement: '' }
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ v-container
|
|||
v-spacer
|
||||
v-text-field(v-model='search'
|
||||
:append-icon='mdiMagnify' outlined rounded
|
||||
label='Search'
|
||||
:label="$t('common.search')"
|
||||
single-line hide-details)
|
||||
v-card-subtitle(v-html="$t('admin.collections_description')")
|
||||
|
||||
|
@ -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="[',', ';']"
|
||||
|
@ -75,6 +75,7 @@ v-container
|
|||
:headers='filterHeaders'
|
||||
:items='filters'
|
||||
:hide-default-footer='filters.length < 5'
|
||||
:header-props='{ sortIcon: mdiChevronDown }'
|
||||
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }')
|
||||
template(v-slot:item.actions='{ item }')
|
||||
v-btn(@click='removeFilter(item)' color='error' icon)
|
||||
|
@ -94,6 +95,7 @@ v-container
|
|||
:headers='collectionHeaders'
|
||||
:items='collections'
|
||||
:hide-default-footer='collections.length < 5'
|
||||
:header-props='{ sortIcon: mdiChevronDown }'
|
||||
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
|
||||
:search='search')
|
||||
template(v-slot:item.filters='{ item }')
|
||||
|
@ -108,12 +110,12 @@ v-container
|
|||
<script>
|
||||
import get from 'lodash/get'
|
||||
import debounce from 'lodash/debounce'
|
||||
import { mdiPencil, mdiChevronLeft, mdiChevronRight, mdiMagnify, mdiPlus, mdiTagMultiple, mdiMapMarker, mdiDeleteForever, mdiCloseCircle } from '@mdi/js'
|
||||
import { mdiPencil, mdiChevronLeft, mdiChevronRight, mdiMagnify, mdiPlus, mdiTagMultiple, mdiMapMarker, mdiDeleteForever, mdiCloseCircle, mdiChevronDown } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
mdiPencil, mdiChevronRight, mdiChevronLeft, mdiMagnify, mdiPlus, mdiTagMultiple, mdiMapMarker, mdiDeleteForever, mdiCloseCircle,
|
||||
mdiPencil, mdiChevronRight, mdiChevronLeft, mdiMagnify, mdiPlus, mdiTagMultiple, mdiMapMarker, mdiDeleteForever, mdiCloseCircle, mdiChevronDown,
|
||||
loading: false,
|
||||
dialog: false,
|
||||
valid: false,
|
||||
|
@ -128,14 +130,14 @@ export default {
|
|||
tagName: '',
|
||||
placeName: '',
|
||||
collectionHeaders: [
|
||||
{ value: 'name', text: 'Name' },
|
||||
{ value: 'filters', text: 'Filters' },
|
||||
{ value: 'actions', text: 'Actions', align: 'right' }
|
||||
{ value: 'name', text: this.$t('common.name') },
|
||||
{ value: 'filters', text: this.$t('common.filter') },
|
||||
{ value: 'actions', text: this.$t('common.actions'), align: 'right' }
|
||||
],
|
||||
filterHeaders: [
|
||||
{ value: 'tags', text: 'Tags' },
|
||||
{ value: 'places', text: 'Places' },
|
||||
{ value: 'actions', text: 'Actions', align: 'right' }
|
||||
{ value: 'tags', text: this.$t('common.tags') },
|
||||
{ value: 'places', text: this.$t('common.places') },
|
||||
{ value: 'actions', text: this.$t('common.actions'), align: 'right' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
@ -5,6 +5,7 @@ v-container
|
|||
v-card-text
|
||||
v-data-table(
|
||||
:hide-default-footer='unconfirmedEvents.length<10'
|
||||
:header-props='{ sortIcon: mdiChevronDown }'
|
||||
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
|
||||
:items='unconfirmedEvents'
|
||||
:headers='headers')
|
||||
|
@ -17,7 +18,7 @@ v-container
|
|||
color='error') {{$t('common.delete')}}
|
||||
</template>
|
||||
<script>
|
||||
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js'
|
||||
import { mdiChevronLeft, mdiChevronRight, mdiChevronDown } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
@ -25,15 +26,15 @@ export default {
|
|||
},
|
||||
data () {
|
||||
return {
|
||||
mdiChevronLeft, mdiChevronRight,
|
||||
mdiChevronLeft, mdiChevronRight, mdiChevronDown,
|
||||
valid: false,
|
||||
dialog: false,
|
||||
editing: false,
|
||||
headers: [
|
||||
{ value: 'title', text: 'Title' },
|
||||
{ value: 'place.name', text: 'Place' },
|
||||
{ value: 'when', text: 'When' },
|
||||
{ value: 'actions', text: 'Actions', align: 'right' }
|
||||
{ value: 'title', text: this.$t('common.title') },
|
||||
{ value: 'place.name', text: this.$t('common.place') },
|
||||
{ value: 'when', text: this.$t('common.when') },
|
||||
{ value: 'actions', text: this.$t('common.actions'), align: 'right' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
@ -40,6 +40,13 @@ v-container
|
|||
@blur='save("instance_place", instance_place)'
|
||||
)
|
||||
|
||||
v-text-field.mt-4(v-model='trusted_instances_label'
|
||||
:label="$t('admin.trusted_instances_label')"
|
||||
persistent-hint inset
|
||||
:hint="$t('admin.trusted_instances_label_help')"
|
||||
@blur='save("trusted_instances_label", trusted_instances_label)'
|
||||
)
|
||||
|
||||
v-dialog(v-model='dialogAddInstance' width='500px' :fullscreen='$vuetify.breakpoint.xsOnly')
|
||||
v-card
|
||||
v-card-title {{$t('admin.add_trusted_instance')}}
|
||||
|
@ -53,14 +60,15 @@ v-container
|
|||
:label="$t('common.url')")
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(color='error' @click='dialogAddInstance=false') {{$t('common.cancel')}}
|
||||
v-btn(color='primary' :disabled='!valid || loading' :loading='loading' @click='createTrustedInstance') {{$t('common.ok')}}
|
||||
v-btn(outlined color='error' @click='dialogAddInstance=false') {{$t('common.cancel')}}
|
||||
v-btn(outlined color='primary' :disabled='!valid || loading' :loading='loading' @click='createTrustedInstance') {{$t('common.ok')}}
|
||||
|
||||
v-btn.mt-4(@click='dialogAddInstance = true' color='primary' text) <v-icon v-text='mdiPlus'></v-icon> {{$t('admin.add_instance')}}
|
||||
v-data-table(
|
||||
v-if='settings.trusted_instances.length'
|
||||
:hide-default-footer='settings.trusted_instances.length<10'
|
||||
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
|
||||
:header-props='{ sortIcon: mdiChevronDown }'
|
||||
:headers='headers'
|
||||
:items='settings.trusted_instances')
|
||||
template(v-slot:item.actions="{item}")
|
||||
|
@ -72,16 +80,17 @@ v-container
|
|||
import { mapActions, mapState } from 'vuex'
|
||||
import get from 'lodash/get'
|
||||
import axios from 'axios'
|
||||
import { mdiDeleteForever, mdiPlus, mdiChevronLeft, mdiChevronRight } from '@mdi/js'
|
||||
import { mdiDeleteForever, mdiPlus, mdiChevronLeft, mdiChevronRight, mdiChevronDown } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
name: 'Federation',
|
||||
data ({ $store, $options }) {
|
||||
return {
|
||||
mdiDeleteForever, mdiPlus, mdiChevronLeft, mdiChevronRight,
|
||||
mdiDeleteForever, mdiPlus, mdiChevronLeft, mdiChevronRight, mdiChevronDown,
|
||||
instance_url: '',
|
||||
instance_name: $store.state.settings.instance_name,
|
||||
instance_place: $store.state.settings.instance_place,
|
||||
trusted_instances_label: $store.state.settings.trusted_instances_label,
|
||||
url2host: $options.filters.url2host,
|
||||
dialogAddInstance: false,
|
||||
loading: false,
|
||||
|
|
169
components/admin/Geolocation.vue
Normal file
169
components/admin/Geolocation.vue
Normal 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>
|
|
@ -10,6 +10,7 @@ v-container
|
|||
:items-per-page='5'
|
||||
:search='instancesFilter'
|
||||
:hide-default-footer='instances.length<5'
|
||||
:header-props='{ sortIcon: mdiChevronDown }'
|
||||
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
|
||||
dense :headers='instancesHeader'
|
||||
@click:row='instanceSelected')
|
||||
|
@ -24,6 +25,7 @@ v-container
|
|||
:search='usersFilter'
|
||||
:hide-default-footer='users.length<5'
|
||||
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
|
||||
:header-props='{ sortIcon: mdiChevronDown }'
|
||||
dense :headers='usersHeader')
|
||||
template(v-slot:item.blocked="{ item }")
|
||||
v-icon(@click='toggleUserBlock(item)' v-text='item.blocked ? mdiCheckboxIntermediate : mdiCheckboxBlankOutline')
|
||||
|
@ -34,6 +36,7 @@ v-container
|
|||
:headers='resourcesHeader'
|
||||
:hide-default-footer='resources.length<10'
|
||||
:items-per-page='10'
|
||||
:header-props='{ sortIcon: mdiChevronDown }'
|
||||
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }')
|
||||
template(v-slot:item.content='{ item }')
|
||||
span(v-html='item.data.content')
|
||||
|
@ -62,33 +65,33 @@ v-container
|
|||
import { mapState, mapActions } from 'vuex'
|
||||
import get from 'lodash/get'
|
||||
import { mdiDelete, mdiEye, mdiEyeOff, mdiDotsVertical, mdiCheckboxIntermediate,
|
||||
mdiCheckboxBlankOutline, mdiChevronLeft, mdiChevronRight } from '@mdi/js'
|
||||
mdiCheckboxBlankOutline, mdiChevronLeft, mdiChevronRight, mdiChevronDown } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
name: 'Moderation',
|
||||
data () {
|
||||
return {
|
||||
mdiDelete, mdiEye, mdiEyeOff, mdiDotsVertical, mdiCheckboxIntermediate,
|
||||
mdiCheckboxBlankOutline, mdiChevronLeft, mdiChevronRight,
|
||||
mdiCheckboxBlankOutline, mdiChevronLeft, mdiChevronRight, mdiChevronDown,
|
||||
instances: [],
|
||||
resources: [],
|
||||
users: [],
|
||||
usersHeader: [
|
||||
{ value: 'object.preferredUsername', text: 'Name' },
|
||||
{ value: 'blocked', text: 'Blocked' }
|
||||
{ value: 'object.preferredUsername', text: this.$t('common.name') },
|
||||
{ value: 'blocked', text: this.$t('admin.blocked') }
|
||||
],
|
||||
instancesHeader: [
|
||||
{ value: 'domain', text: 'Domain' },
|
||||
{ value: 'name', text: 'Name' },
|
||||
{ value: 'blocked', text: 'Blocked' },
|
||||
{ value: 'users', text: 'known users' }
|
||||
{ value: 'domain', text: this.$t('admin.domain') },
|
||||
{ value: 'name', text: this.$t('common.name') },
|
||||
{ value: 'blocked', text: this.$t('admin.blocked') },
|
||||
{ value: 'users', text: this.$t('admin.known_users') }
|
||||
],
|
||||
resourcesHeader: [
|
||||
{ value: 'created', text: 'Created' },
|
||||
{ value: 'event', text: 'Event' },
|
||||
{ value: 'user', text: 'user' },
|
||||
{ value: 'content', text: 'Content' },
|
||||
{ value: 'actions', text: 'Actions' }
|
||||
{ value: 'created', text: this.$t('admin.created_at') },
|
||||
{ value: 'event', text: this.$t('common.event') },
|
||||
{ value: 'user', text: this.$t('common.user') },
|
||||
{ value: 'content', text: this.$t('common.content') },
|
||||
{ value: 'actions', text: this.$t('common.actions') }
|
||||
],
|
||||
usersFilter: '',
|
||||
instancesFilter: ''
|
||||
|
|
|
@ -4,7 +4,7 @@ v-container
|
|||
v-spacer
|
||||
v-text-field(v-model='search'
|
||||
:append-icon='mdiMagnify' outlined rounded
|
||||
label='Search'
|
||||
:label="$t('common.search')"
|
||||
single-line hide-details)
|
||||
v-card-subtitle(v-html="$t('admin.place_description')")
|
||||
|
||||
|
@ -19,11 +19,24 @@ v-container
|
|||
v-model='place.name'
|
||||
:placeholder='$t("common.name")')
|
||||
|
||||
v-text-field(
|
||||
:rules="[$validators.required('common.address')]"
|
||||
v-combobox(ref='address'
|
||||
:prepend-icon='mdiMapSearch'
|
||||
@input.native='searchAddress'
|
||||
:label="$t('common.address')"
|
||||
v-model='place.address'
|
||||
:placeholder='$t("common.address")')
|
||||
:rules="[ v => $validators.required('common.address')(v)]"
|
||||
:value='place.address'
|
||||
persistent-hint hide-no-data clearable no-filter
|
||||
:loading='loading'
|
||||
@change='selectAddress'
|
||||
@focus='searchAddress'
|
||||
:items="addressList"
|
||||
:hint="$t('event.address_description')")
|
||||
template(v-slot:item="{ item, attrs, on }")
|
||||
v-list-item(v-bind='attrs' v-on='on')
|
||||
v-list-item-content(two-line v-if='item')
|
||||
v-list-item-title(v-text='item.name')
|
||||
v-list-item-subtitle(v-text='`${item.address}`')
|
||||
|
||||
|
||||
v-card-actions
|
||||
v-spacer
|
||||
|
@ -36,8 +49,11 @@ v-container
|
|||
:headers='headers'
|
||||
:items='places'
|
||||
:hide-default-footer='places.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='editPlace(item)' color='primary' icon)
|
||||
v-icon(v-text='mdiPencil')
|
||||
|
@ -46,32 +62,46 @@ v-container
|
|||
|
||||
</template>
|
||||
<script>
|
||||
import { mdiPencil, mdiChevronLeft, mdiChevronRight, mdiMagnify, mdiEye } from '@mdi/js'
|
||||
import { mdiPencil, mdiChevronLeft, mdiChevronRight, mdiMagnify, mdiEye, mdiMapSearch, mdiChevronDown } from '@mdi/js'
|
||||
import { mapState } from 'vuex'
|
||||
import debounce from 'lodash/debounce'
|
||||
import get from 'lodash/get'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
data( {$store} ) {
|
||||
return {
|
||||
mdiPencil, mdiChevronRight, mdiChevronLeft, mdiMagnify, mdiEye,
|
||||
mdiPencil, mdiChevronRight, mdiChevronLeft, mdiMagnify, mdiEye, mdiMapSearch, mdiChevronDown,
|
||||
loading: false,
|
||||
dialog: false,
|
||||
valid: false,
|
||||
places: [],
|
||||
addressList: [],
|
||||
address: '',
|
||||
search: '',
|
||||
place: { name: '', address: '', id: null },
|
||||
headers: [
|
||||
{ value: 'name', text: 'Name' },
|
||||
{ value: 'address', text: 'Address' },
|
||||
{ value: 'actions', text: 'Actions', align: 'right' }
|
||||
]
|
||||
{ value: 'name', text: this.$t('common.name') },
|
||||
{ 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']),
|
||||
},
|
||||
methods: {
|
||||
editPlace(item) {
|
||||
this.place.name = item.name
|
||||
this.place.address = item.address
|
||||
if (this.settings.allow_geolocation) {
|
||||
this.place.latitude = item.latitude
|
||||
this.place.longitude = item.longitude
|
||||
}
|
||||
this.place.id = item.id
|
||||
this.dialog = true
|
||||
},
|
||||
|
@ -82,7 +112,107 @@ export default {
|
|||
await this.$fetch()
|
||||
this.loading = false
|
||||
this.dialog = false
|
||||
}
|
||||
},
|
||||
selectAddress (v) {
|
||||
if (!v) { return }
|
||||
if (typeof v === 'object') {
|
||||
this.place.latitude = v.lat
|
||||
this.place.longitude = v.lon
|
||||
this.place.address = v.address
|
||||
// }
|
||||
} else {
|
||||
this.place.address = v
|
||||
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
|
||||
const searchCoordinates = pre_searchCoordinates.replace('/', ',')
|
||||
// const regex_coords_comma = "-?[1-9][0-9]*(\\.[0-9]+)?,\\s*-?[1-9][0-9]*(\\.[0-9]+)?";
|
||||
// const regex_coords_slash = "-?[1-9][0-9]*(\\.[0-9]+)?/\\s*-?[1-9][0-9]*(\\.[0-9]+)?";
|
||||
|
||||
// const setCoords = (v) => {
|
||||
// const lat = v[0].trim()
|
||||
// const lon = v[1].trim()
|
||||
// // check coordinates are valid
|
||||
// if ((lat < 90 && lat > -90)
|
||||
// && (lon < 180 && lon > -180)) {
|
||||
// this.place.latitude = lat
|
||||
// this.place.longitude = lon
|
||||
// } else {
|
||||
// this.$root.$message("Non existent coordinates", { color: 'error' })
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (pre_searchCoordinates.match(regex_coords_comma)) {
|
||||
// let v = pre_searchCoordinates.split(",")
|
||||
// setCoords(v)
|
||||
// return
|
||||
// }
|
||||
// if (pre_searchCoordinates.match(regex_coords_slash)) {
|
||||
// let v = pre_searchCoordinates.split("/")
|
||||
// setCoords(v)
|
||||
// return
|
||||
// }
|
||||
|
||||
if (searchCoordinates.length) {
|
||||
this.loading = true
|
||||
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)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
86
components/admin/Plugin.vue
Normal file
86
components/admin/Plugin.vue
Normal file
|
@ -0,0 +1,86 @@
|
|||
<template lang='pug'>
|
||||
v-container
|
||||
v-card-title {{ $t('common.plugins') }}
|
||||
v-spacer
|
||||
v-card-subtitle(v-html="$t('admin.plugins_description')")
|
||||
v-dialog(v-if='selectedPlugin.settingsValue' v-model='dialog' width='600' :fullscreen='$vuetify.breakpoint.xsOnly')
|
||||
v-card
|
||||
v-card-title {{ $t('admin.config_plugin') }} - {{ selectedPlugin.name }}
|
||||
v-card-text
|
||||
v-form(v-model='valid' ref='form' lazy-validation)
|
||||
v-row(v-for='(setting, name) in selectedPlugin.settings' :key='name' mt-2)
|
||||
v-col.col-4
|
||||
small(v-html='setting.hint')
|
||||
v-col.col-8
|
||||
v-text-field(v-if='setting.type === "TEXT"' v-model='selectedPlugin.settingsValue[name]'
|
||||
type='text' :label='setting.description'
|
||||
persistent-hint
|
||||
:rules="[setting.required ? $validators.required(setting.description) : false]")
|
||||
|
||||
v-text-field(v-if='setting.type === "NUMBER"' v-model='selectedPlugin.settingsValue[name]' type='number' :label='setting.description')
|
||||
v-switch(v-if='setting.type === "CHECK"' v-model='selectedPlugin.settingsValue[name]' :label='setting.description')
|
||||
v-select(v-if='setting.type === "LIST"' v-model='selectedPlugin.settingsValue[name]' :items='setting.items' :label='setting.description')
|
||||
v-switch(:label="$t('common.enable')" inset color='primary' v-model='selectedPlugin.settingsValue["enable"]')
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(@click='dialog = false' outlined color='warning') {{ $t('common.cancel') }}
|
||||
v-btn(@click='saveSettings' outlined color='primary' :loading='loading'
|
||||
:disable='!valid || loading') {{ $t('common.save') }}
|
||||
|
||||
v-card-text
|
||||
v-card(v-for='plugin in plugins' :key='plugin.name' max-width="400" elevation='10' color='secondary' dark)
|
||||
v-card-title {{ plugin.name }}
|
||||
v-card-text
|
||||
p {{ plugin.description }}
|
||||
blockquote author: {{ plugin.author }}
|
||||
a(:href='plugin.url' v-text='plugin.url')
|
||||
v-row
|
||||
v-spacer
|
||||
v-btn(text color='primary' @click='setOptions(plugin)') {{ $t('common.settings') }}
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import { mdiPencil, mdiChevronLeft, mdiChevronRight, mdiMagnify, mdiEye } from '@mdi/js'
|
||||
import { mapActions, mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
mdiPencil, mdiChevronRight, mdiChevronLeft, mdiMagnify, mdiEye,
|
||||
loading: false,
|
||||
dialog: false,
|
||||
valid: false,
|
||||
selectedPlugin: {},
|
||||
plugins: [],
|
||||
headers: [
|
||||
{ value: 'name', text: 'Name' },
|
||||
{ value: 'description', text: 'Address' },
|
||||
{ value: 'actions', text: 'Actions', align: 'right' }
|
||||
]
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.plugins = await this.$axios.$get('/plugins')
|
||||
},
|
||||
computed: mapState(['settings']),
|
||||
methods: {
|
||||
...mapActions(['setSetting']),
|
||||
async saveSettings() {
|
||||
this.loading = true
|
||||
this.setSetting({
|
||||
key: 'plugin_' + this.selectedPlugin.name,
|
||||
value: this.selectedPlugin.settingsValue
|
||||
})
|
||||
this.loading = false
|
||||
this.dialog = false
|
||||
},
|
||||
async toggleEnable(plugin) {
|
||||
await this.$axios.$put(`/plugin/${plugin.name}`)
|
||||
},
|
||||
setOptions(plugin) {
|
||||
this.selectedPlugin = plugin
|
||||
this.dialog = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -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'
|
||||
|
|
|
@ -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')")
|
||||
|
@ -48,37 +52,44 @@ v-container
|
|||
inset
|
||||
:label="$t('admin.recurrent_event_visible')")
|
||||
|
||||
v-switch.mt-1(v-model='allow_geolocation'
|
||||
inset
|
||||
:label="$t('admin.allow_geolocation')")
|
||||
|
||||
v-dialog(v-model='showSMTP' destroy-on-close max-width='700px' :fullscreen='$vuetify.breakpoint.xsOnly')
|
||||
SMTP(@close='showSMTP = false')
|
||||
|
||||
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 locales from '../../locales/esm'
|
||||
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: {
|
||||
|
@ -103,10 +114,18 @@ 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 }) }
|
||||
},
|
||||
allow_geolocation: {
|
||||
get () { return this.settings.allow_geolocation },
|
||||
set (value) { this.setSetting({ key: 'allow_geolocation', value }) }
|
||||
},
|
||||
filteredTimezones () {
|
||||
const current_timezone = moment.tz.guess()
|
||||
tzNames.unshift(current_timezone)
|
||||
|
|
115
components/admin/Tags.vue
Normal file
115
components/admin/Tags.vue
Normal file
|
@ -0,0 +1,115 @@
|
|||
<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 debounce from 'lodash/debounce'
|
||||
import get from 'lodash/get'
|
||||
|
||||
export default {
|
||||
data( {$store} ) {
|
||||
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>
|
|
@ -2,19 +2,56 @@
|
|||
v-container
|
||||
v-card-title {{$t('common.theme')}}
|
||||
v-card-text
|
||||
//- LOGO
|
||||
v-file-input.mt-5(ref='upload'
|
||||
:label="$t('admin.favicon')"
|
||||
@change='uploadLogo'
|
||||
accept='image/*')
|
||||
template(slot='append-outer')
|
||||
v-btn(color='warning' text @click='resetLogo') <v-icon v-text='mdiRestore'></v-icon> {{$t('common.reset')}}
|
||||
v-img(:src='`/logo.png?${logoKey}`'
|
||||
max-width="60px" max-height="60px" contain)
|
||||
|
||||
v-switch.mt-5(v-model='is_dark'
|
||||
inset
|
||||
:label="$t('admin.is_dark')")
|
||||
|
||||
v-switch.mt-5(v-model='hide_thumbs'
|
||||
inset
|
||||
:label="$t('admin.hide_thumbs')")
|
||||
|
||||
v-switch.mt-5(v-model='hide_calendar'
|
||||
inset
|
||||
: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')
|
||||
//- LOGO
|
||||
v-file-input.mt-5(ref='upload'
|
||||
:label="$t('admin.favicon')"
|
||||
@change='uploadLogo'
|
||||
accept='image/*')
|
||||
template(slot='append-outer')
|
||||
v-btn(color='warning' text @click='resetLogo') <v-icon v-text='mdiRestore'></v-icon> {{$t('common.reset')}}
|
||||
v-img.mt-2(:src='`/logo.png?${logoKey}`' max-height="60px" contain)
|
||||
|
||||
v-col(cols='4')
|
||||
//- FALLBACK IMAGE
|
||||
v-file-input.mt-5(ref='upload'
|
||||
:label="$t('admin.fallback_image')"
|
||||
persistent-hint
|
||||
@change='uploadFallbackImage'
|
||||
accept='image/*')
|
||||
template(slot='append-outer')
|
||||
v-btn(color='warning' text @click='resetFallbackImage') <v-icon v-text='mdiRestore'></v-icon> {{$t('common.reset')}}
|
||||
v-img.mt-2(:src='`/fallbackimage.png?${fallbackImageKey}`' max-height="150px" contain)
|
||||
|
||||
v-col(cols='4')
|
||||
//- HEADER IMAGE
|
||||
v-file-input.mt-5(ref='upload'
|
||||
:label="$t('admin.header_image')"
|
||||
persistent-hint
|
||||
@change='uploadHeaderImage'
|
||||
accept='image/*')
|
||||
template(slot='append-outer')
|
||||
v-btn(color='warning' text @click='resetHeaderImage') <v-icon v-text='mdiRestore'></v-icon> {{$t('common.reset')}}
|
||||
v-img.mt-2(:src='`/headerimage.png?${headerImageKey}`' max-height="150px" contain)
|
||||
|
||||
|
||||
|
||||
//- TODO choose theme colors
|
||||
//- v-row
|
||||
|
@ -45,8 +82,8 @@ v-container
|
|||
:label="$t('common.url')")
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(link @click='linkModal=false' color='error') {{$t('common.cancel')}}
|
||||
v-btn(link @click='addFooterLink' color='primary' :disabled='!valid') {{$t('common.add')}}
|
||||
v-btn(outlined @click='linkModal=false' color='error') {{$t('common.cancel')}}
|
||||
v-btn(outlined @click='addFooterLink' color='primary' :disabled='!valid') {{$t('common.add')}}
|
||||
|
||||
v-card-title {{$t('admin.footer_links')}}
|
||||
v-card-text
|
||||
|
@ -73,10 +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,
|
||||
logoKey: t,
|
||||
fallbackImageKey: t,
|
||||
headerImageKey: t,
|
||||
link: { href: '', label: '' },
|
||||
linkModal: false
|
||||
// menu: [false, false, false, false]
|
||||
|
@ -97,7 +137,15 @@ export default {
|
|||
this.$vuetify.theme.dark = value
|
||||
this.setSetting({ key: 'theme.is_dark', value })
|
||||
}
|
||||
}
|
||||
},
|
||||
hide_thumbs: {
|
||||
get () { return this.settings.hide_thumbs },
|
||||
set (value) { this.setSetting({ key: 'hide_thumbs', value }) }
|
||||
},
|
||||
hide_calendar: {
|
||||
get () { return this.settings.hide_calendar },
|
||||
set (value) { this.setSetting({ key: 'hide_calendar', value }) }
|
||||
},
|
||||
// 'colors[0]': {
|
||||
// get () {
|
||||
// return this.settings['theme.colors'] || [0, 0]
|
||||
|
@ -120,18 +168,35 @@ export default {
|
|||
this.setSetting({
|
||||
key: 'footerLinks',
|
||||
value: [
|
||||
{ href: '/about', label: 'about' },
|
||||
{ href: '/', label: 'home' }]
|
||||
{ href: '/', label: 'common.home' },
|
||||
{ href: '/about', label: 'common.about' }
|
||||
]
|
||||
})
|
||||
},
|
||||
forceLogoReload () {
|
||||
this.logoKey++
|
||||
},
|
||||
forceFallbackImageReload () {
|
||||
this.fallbackImageKey++
|
||||
},
|
||||
forceHeaderImageReload () {
|
||||
this.headerImageKey++
|
||||
},
|
||||
resetLogo (e) {
|
||||
this.setSetting({ key: 'logo', value: null })
|
||||
.then(this.forceLogoReload)
|
||||
e.stopPropagation()
|
||||
},
|
||||
resetFallbackImage (e) {
|
||||
this.setSetting({ key: 'fallback_image', value: null })
|
||||
.then(this.forceFallbackImageReload)
|
||||
e.stopPropagation()
|
||||
},
|
||||
resetHeaderImage (e) {
|
||||
this.setSetting({ key: 'header_image', value: null })
|
||||
.then(this.forceHeaderImageReload)
|
||||
e.stopPropagation()
|
||||
},
|
||||
updateColor (i, v) {
|
||||
this.colors[i] = v.hex
|
||||
this.$vuetify.theme.themes.dark[i] = v.hex
|
||||
|
@ -177,6 +242,32 @@ export default {
|
|||
|
||||
}
|
||||
},
|
||||
async uploadFallbackImage (file) {
|
||||
const formData = new FormData()
|
||||
formData.append('fallbackImage', file)
|
||||
try {
|
||||
await this.$axios.$post('/settings/fallbackImage', formData)
|
||||
this.$root.$emit('message', {
|
||||
message: 'Fallback image updated'
|
||||
})
|
||||
this.forceFallbackImageReload()
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
},
|
||||
async uploadHeaderImage (file) {
|
||||
const formData = new FormData()
|
||||
formData.append('headerImage', file)
|
||||
try {
|
||||
await this.$axios.$post('/settings/headerImage', formData)
|
||||
this.$root.$emit('message', {
|
||||
message: 'Header image updated'
|
||||
})
|
||||
this.forceHeaderImageReload()
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
},
|
||||
save (key, value) {
|
||||
if (this.settings[key] !== value) {
|
||||
this.setSetting({ key, value })
|
||||
|
|
|
@ -4,7 +4,7 @@ v-container
|
|||
v-spacer
|
||||
v-text-field(v-model='search'
|
||||
:append-icon='mdiMagnify' outlined rounded
|
||||
label='Search'
|
||||
:label="$t('common.search')"
|
||||
single-line hide-details)
|
||||
|
||||
v-btn(color='primary' text @click='newUserDialog = true') <v-icon v-text='mdiPlus'></v-icon> {{$t('common.new_user')}}
|
||||
|
@ -32,6 +32,7 @@ v-container
|
|||
:headers='headers'
|
||||
:items='users'
|
||||
:hide-default-footer='users.length<5'
|
||||
:header-props='{ sortIcon: mdiChevronDown }'
|
||||
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
|
||||
:search='search')
|
||||
template(v-slot:item.is_active='{item}')
|
||||
|
@ -49,7 +50,7 @@ v-container
|
|||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import get from 'lodash/get'
|
||||
import { mdiClose, mdiMagnify, mdiCheck, mdiPlus, mdiInformation, mdiChevronLeft, mdiChevronRight } from '@mdi/js'
|
||||
import { mdiClose, mdiMagnify, mdiCheck, mdiPlus, mdiInformation, mdiChevronLeft, mdiChevronRight, mdiChevronDown } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
name: 'Users',
|
||||
|
@ -58,7 +59,7 @@ export default {
|
|||
},
|
||||
data () {
|
||||
return {
|
||||
mdiClose, mdiMagnify, mdiCheck, mdiPlus, mdiInformation, mdiChevronLeft, mdiChevronRight,
|
||||
mdiClose, mdiMagnify, mdiCheck, mdiPlus, mdiInformation, mdiChevronLeft, mdiChevronRight, mdiChevronDown,
|
||||
newUserDialog: false,
|
||||
valid: false,
|
||||
new_user: {
|
||||
|
@ -67,10 +68,10 @@ export default {
|
|||
},
|
||||
search: '',
|
||||
headers: [
|
||||
{ value: 'email', text: 'Email' },
|
||||
{ value: 'description', text: 'Description' },
|
||||
{ value: 'email', text: this.$t('common.email') },
|
||||
{ value: 'description', text: this.$t('common.description') },
|
||||
{ value: 'is_active', text: 'Active' },
|
||||
{ value: 'actions', text: 'Actions', align: 'right' }
|
||||
{ value: 'actions', text: this.$t('common.actions'), align: 'right' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
@ -372,7 +372,6 @@
|
|||
"Brazil/East",
|
||||
"Brazil/West",
|
||||
"CET",
|
||||
"CST6CDT",
|
||||
"Canada/Atlantic",
|
||||
"Canada/Central",
|
||||
"Canada/Eastern",
|
||||
|
@ -384,46 +383,6 @@
|
|||
"Chile/Continental",
|
||||
"Chile/EasterIsland",
|
||||
"Cuba",
|
||||
"EET",
|
||||
"EST",
|
||||
"EST5EDT",
|
||||
"Egypt",
|
||||
"Eire",
|
||||
"Etc/GMT",
|
||||
"Etc/GMT+0",
|
||||
"Etc/GMT+1",
|
||||
"Etc/GMT+10",
|
||||
"Etc/GMT+11",
|
||||
"Etc/GMT+12",
|
||||
"Etc/GMT+2",
|
||||
"Etc/GMT+3",
|
||||
"Etc/GMT+4",
|
||||
"Etc/GMT+5",
|
||||
"Etc/GMT+6",
|
||||
"Etc/GMT+7",
|
||||
"Etc/GMT+8",
|
||||
"Etc/GMT+9",
|
||||
"Etc/GMT-0",
|
||||
"Etc/GMT-1",
|
||||
"Etc/GMT-10",
|
||||
"Etc/GMT-11",
|
||||
"Etc/GMT-12",
|
||||
"Etc/GMT-13",
|
||||
"Etc/GMT-14",
|
||||
"Etc/GMT-2",
|
||||
"Etc/GMT-3",
|
||||
"Etc/GMT-4",
|
||||
"Etc/GMT-5",
|
||||
"Etc/GMT-6",
|
||||
"Etc/GMT-7",
|
||||
"Etc/GMT-8",
|
||||
"Etc/GMT-9",
|
||||
"Etc/GMT0",
|
||||
"Etc/Greenwich",
|
||||
"Etc/UCT",
|
||||
"Etc/UTC",
|
||||
"Etc/Universal",
|
||||
"Etc/Zulu",
|
||||
"Europe/Amsterdam",
|
||||
"Europe/Andorra",
|
||||
"Europe/Astrakhan",
|
||||
|
@ -489,10 +448,6 @@
|
|||
"Europe/Zurich",
|
||||
"GB",
|
||||
"GB-Eire",
|
||||
"GMT",
|
||||
"GMT+0",
|
||||
"GMT-0",
|
||||
"GMT0",
|
||||
"Greenwich",
|
||||
"HST",
|
||||
"Hongkong",
|
||||
|
@ -514,17 +469,11 @@
|
|||
"Japan",
|
||||
"Kwajalein",
|
||||
"Libya",
|
||||
"MET",
|
||||
"MST",
|
||||
"MST7MDT",
|
||||
"Mexico/BajaNorte",
|
||||
"Mexico/BajaSur",
|
||||
"Mexico/General",
|
||||
"NZ",
|
||||
"NZ-CHAT",
|
||||
"Navajo",
|
||||
"PRC",
|
||||
"PST8PDT",
|
||||
"Pacific/Apia",
|
||||
"Pacific/Auckland",
|
||||
"Pacific/Bougainville",
|
||||
|
@ -570,8 +519,6 @@
|
|||
"Pacific/Yap",
|
||||
"Poland",
|
||||
"Portugal",
|
||||
"ROC",
|
||||
"ROK",
|
||||
"Singapore",
|
||||
"Turkey",
|
||||
"UCT",
|
||||
|
@ -589,7 +536,5 @@
|
|||
"US/Samoa",
|
||||
"UTC",
|
||||
"Universal",
|
||||
"W-SU",
|
||||
"WET",
|
||||
"Zulu"
|
||||
]
|
|
@ -11,8 +11,8 @@ v-card
|
|||
gancio-event(:id='event.id' :baseurl='settings.baseurl')
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(text color='warning' @click="$emit('close')") {{$t("common.cancel")}}
|
||||
v-btn(text @click='clipboard(code)' color="primary") {{$t("common.copy")}}
|
||||
v-btn(outlined color='warning' @click="$emit('close')") {{$t("common.close")}}
|
||||
v-btn(outlined @click='clipboard(code)' color="primary") {{$t("common.copy")}}
|
||||
</template>
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
|
|
@ -1,24 +1,78 @@
|
|||
<template lang='pug'>
|
||||
div
|
||||
v-btn(text color='primary' v-if='event.is_visible' @click='toggle(false)') {{$t(`common.${event.parentId?'skip':'hide'}`)}}
|
||||
v-btn(text color='success' v-else @click='toggle(false)') <v-icon color='yellow' v-text='mdiAlert'></v-icon> {{$t('common.confirm')}}
|
||||
v-btn(text color='primary' @click='$router.push(`/add/${event.id}`)') {{$t('common.edit')}}
|
||||
v-btn(text color='primary' v-if='!event.parentId' @click='remove(false)') {{$t('common.remove')}}
|
||||
span
|
||||
v-list(dense nav)
|
||||
v-list-group(:append-icon='mdiChevronUp' :value='true')
|
||||
template(v-slot:activator)
|
||||
v-list-item.text-overline {{$t('common.admin_actions')}}
|
||||
|
||||
template(v-if='event.parentId')
|
||||
v-divider
|
||||
span.mr-1 <v-icon v-text='mdiRepeat'></v-icon> {{$t('event.edit_recurrent')}}
|
||||
v-btn(text color='primary' v-if='event.parent.is_visible' @click='toggle(true)') {{$t('common.pause')}}
|
||||
v-btn(text color='primary' v-else @click='toggle(true)') {{$t('common.start')}}
|
||||
v-btn(text color='primary' @click='$router.push(`/add/${event.parentId}`)') {{$t('common.edit')}}
|
||||
v-btn(text color='primary' @click='remove(true)') {{$t('common.remove')}}
|
||||
//- Hide / confirm event
|
||||
v-list-item(@click='toggle(false)')
|
||||
v-list-item-icon
|
||||
v-icon(v-if='event.is_visible' v-text='mdiEyeOff')
|
||||
v-icon(v-else='event.is_visible' v-text='mdiEye')
|
||||
v-list-item-content
|
||||
v-list-item-title(v-text="$t(`common.${event.is_visible?'hide':'confirm'}`)")
|
||||
|
||||
//- Edit event
|
||||
v-list-item(:to='`/add/${event.id}`')
|
||||
v-list-item-icon
|
||||
v-icon(v-text='mdiCalendarEdit')
|
||||
v-list-item-content
|
||||
v-list-item-title(v-text="$t('common.edit')")
|
||||
|
||||
//- Remove
|
||||
v-list-item(@click='remove(false)')
|
||||
v-list-item-icon
|
||||
v-icon(v-text='mdiDelete')
|
||||
v-list-item-content
|
||||
v-list-item-title(v-text="$t('common.remove')")
|
||||
|
||||
|
||||
template(v-if='event.parentId')
|
||||
v-list-item.text-overline(v-html="$t('common.recurring_event_actions')")
|
||||
|
||||
//- Pause / Start to generate recurring event
|
||||
v-list-item(@click='toggle(true)')
|
||||
v-list-item-icon
|
||||
v-icon(v-text='event.parent.is_visible ? mdiPause : mdiPlay')
|
||||
v-list-item-content
|
||||
v-list-item-title(v-text="$t(`common.${event.parent.is_visible ? 'pause': 'start'}`)")
|
||||
|
||||
|
||||
//- Edit event
|
||||
v-list-item(:to='`/add/${event.parentId}`')
|
||||
v-list-item-icon
|
||||
v-icon(v-text='mdiCalendarEdit')
|
||||
v-list-item-content
|
||||
v-list-item-title(v-text="$t('common.edit')")
|
||||
|
||||
//- Remove
|
||||
v-list-item(@click='remove(true)')
|
||||
v-list-item-icon
|
||||
v-icon(v-text='mdiDeleteForever')
|
||||
v-list-item-content
|
||||
v-list-item-title(v-text="$t('common.remove')")
|
||||
|
||||
|
||||
//- v-btn(text color='primary' v-if='event.is_visible' @click='toggle(false)') {{$t(`common.${event.parentId?'skip':'hide'}`)}}
|
||||
//- v-btn(text color='success' v-else @click='toggle(false)') <v-icon color='yellow' v-text='mdiAlert'></v-icon> {{$t('common.confirm')}}
|
||||
//- v-btn(text color='primary' @click='$router.push(`/add/${event.id}`)') {{$t('common.edit')}}
|
||||
//- v-btn(text color='primary' v-if='!event.parentId' @click='remove(false)') {{$t('common.remove')}}
|
||||
|
||||
//- template(v-if='event.parentId')
|
||||
//- v-divider
|
||||
//- span.mr-1 <v-icon v-text='mdiRepeat'></v-icon> {{$t('event.edit_recurrent')}}
|
||||
//- v-btn(text color='primary' v-if='event.parent.is_visible' @click='toggle(true)') {{$t('common.pause')}}
|
||||
//- v-btn(text color='primary' v-else @click='toggle(true)') {{$t('common.start')}}
|
||||
//- v-btn(text color='primary' @click='$router.push(`/add/${event.parentId}`)') {{$t('common.edit')}}
|
||||
//- v-btn(text color='primary' @click='remove(true)') {{$t('common.remove')}}
|
||||
</template>
|
||||
<script>
|
||||
import { mdiAlert, mdiRepeat } from '@mdi/js'
|
||||
import { mdiChevronUp, mdiRepeat, mdiDelete, mdiCalendarEdit, mdiEyeOff, mdiEye, mdiPause, mdiPlay, mdiDeleteForever } from '@mdi/js'
|
||||
export default {
|
||||
name: 'EventAdmin',
|
||||
data () {
|
||||
return { mdiAlert, mdiRepeat }
|
||||
return { mdiChevronUp, mdiRepeat, mdiDelete, mdiCalendarEdit, mdiEyeOff, mdiEye, mdiPause, mdiPlay, mdiDeleteForever }
|
||||
},
|
||||
props: {
|
||||
event: {
|
||||
|
|
|
@ -313,13 +313,13 @@ function Le(t) {
|
|||
let e, i, n;
|
||||
return {
|
||||
c() {
|
||||
e = g("img"), a(e, "style", "aspect-ratio=1.7778;"), a(e, "alt", i = t[12].title), G(e.src, n = t[0] + "/noimg.svg") || a(e, "src", n), a(e, "loading", "lazy");
|
||||
e = g("img"), a(e, "style", "aspect-ratio=1.7778;"), a(e, "alt", i = t[12].title), G(e.src, n = t[0] + "/fallbackimage.png") || a(e, "src", n), a(e, "loading", "lazy");
|
||||
},
|
||||
m(l, o) {
|
||||
v(l, e, o);
|
||||
},
|
||||
p(l, o) {
|
||||
o & 32 && i !== (i = l[12].title) && a(e, "alt", i), o & 1 && !G(e.src, n = l[0] + "/noimg.svg") && a(e, "src", n);
|
||||
o & 32 && i !== (i = l[12].title) && a(e, "alt", i), o & 1 && !G(e.src, n = l[0] + "/fallbackimage.png") && a(e, "src", n);
|
||||
},
|
||||
d(l) {
|
||||
l && x(e);
|
||||
|
@ -454,7 +454,7 @@ function Re(t, e, i) {
|
|||
}), t.$$set = (d) => {
|
||||
"baseurl" in d && i(0, n = d.baseurl), "title" in d && i(1, l = d.title), "maxlength" in d && i(6, o = d.maxlength), "tags" in d && i(7, r = d.tags), "places" in d && i(8, f = d.places), "theme" in d && i(2, c = d.theme), "show_recurrent" in d && i(9, s = d.show_recurrent), "sidebar" in d && i(3, k = d.sidebar), "external_style" in d && i(4, m = d.external_style);
|
||||
}, t.$$.update = () => {
|
||||
t.$$.dirty & 974 && w();
|
||||
t.$$.dirty & 975 && w();
|
||||
}, [
|
||||
n,
|
||||
l,
|
||||
|
|
|
@ -8,6 +8,40 @@ nav_order: 10
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
### 1.6.1 - 15 dec '22
|
||||
- allow edit tags in admin panel, fix #170
|
||||
- fix header / fallback image upload, fix #222
|
||||
- fix WPGancio MU
|
||||
- fix recurrent events label
|
||||
- update translations (de, es, eu, gl)
|
||||
|
||||
### 1.6.0 - 11 dec '22
|
||||
- new plugin system - fix #177
|
||||
- new "publish on telegram" plugin: (thanks @fadelkon)
|
||||
- people can now choose the language displayed - fix #171
|
||||
- admin could choose a custom fallback image - fix #195
|
||||
- it is now possible NOT to enter the end time of an event - fix #188
|
||||
- live search
|
||||
- improve event import
|
||||
- add Apple touch icon - fix #200
|
||||
- add nominatim / openstreetmap search feature (thanks @sedum)
|
||||
- new hide calendar option
|
||||
- new hide thumbs from homepage option
|
||||
- linkable admin tab
|
||||
- friendly instances label is now customizable (thanks @sedum)
|
||||
- i18n refactoring
|
||||
- Wordpress plugin now supports MU installation
|
||||
- new chinese translation
|
||||
- new portuguese translation
|
||||
- improved navbar layout
|
||||
- improved event layout
|
||||
- complete oauth2 refactoring
|
||||
- fix ics unique uuid
|
||||
- fix place "[Object]" issue - #194
|
||||
- fix random restart while downloading random media
|
||||
- fix mobile dialog layout
|
||||
- urlencode place and tag urls
|
||||
|
||||
### 1.5.6 - 22 set '22
|
||||
- update linkifyjs, sequelizem, nuxt deps
|
||||
- improve homepage loading time
|
||||
|
|
|
@ -11,4 +11,5 @@ nav_order: 9
|
|||
- :elephant: Mastodon ⇒ [@gancio@mastodon.cisti.org](https://mastodon.cisti.org/@gancio)
|
||||
- :email: Email ⇒ [info@cisti.org](mailto:info@cisti.org)
|
||||
- IRC ⇒ #gancio @ irc.autistici.org (sometimes...)
|
||||
- Issues ⇒ [https://framagit.org/les/gancio/-/issues](https://framagit.org/les/gancio/-/issues)
|
||||
|
||||
|
|
7
docs/docker/nominatim/.env.example
Normal file
7
docs/docker/nominatim/.env.example
Normal file
|
@ -0,0 +1,7 @@
|
|||
|
||||
NOMINATIM_PASSWORD=CeMA4M1kiDo0k
|
||||
|
||||
# Choose PBF_PATH to import a local file
|
||||
PBF_PATH=/nominatim/data/default.osm.pbf
|
||||
# PBF_URL= https://download.geofabrik.de/europe/italy/nord-est-latest.osm.pbf
|
||||
# REPLICATION_URL= https://download.geofabrik.de/europe/italy/nord-est-updates/
|
19
docs/docker/nominatim/docker-compose.yml
Normal file
19
docs/docker/nominatim/docker-compose.yml
Normal file
|
@ -0,0 +1,19 @@
|
|||
version: '3'
|
||||
|
||||
services:
|
||||
nominatim:
|
||||
container_name: nominatim
|
||||
image: mediagis/nominatim:4.2
|
||||
restart: always
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
# see https://github.com/mediagis/nominatim-docker/tree/master/4.2#configuration for more options
|
||||
PBF_PATH: "${PBF_PATH}"
|
||||
PBF_URL: "${PBF_URL}"
|
||||
REPLICATION_URL: "${REPLICATION_URL}"
|
||||
NOMINATIM_PASSWORD: ${NOMINATIM_PASSWORD}
|
||||
volumes:
|
||||
- ./nominatim-data:/var/lib/postgresql/14/main
|
||||
- ./nominatim/data/"${PBF_PATH}":/nominatim/data/"${PBF_PATH}"
|
||||
shm_size: 1gb
|
|
@ -16,7 +16,7 @@ The configuration file shoud be a `.json` or a `.js` file and could be specified
|
|||
1. TOC
|
||||
{:toc}
|
||||
|
||||
- ### Server
|
||||
### Server
|
||||
This probably support unix socket too
|
||||
|
||||
```json
|
||||
|
@ -26,7 +26,7 @@ This probably support unix socket too
|
|||
}
|
||||
```
|
||||
|
||||
- ### Database
|
||||
### Database
|
||||
DB configuration, look [here](https://sequelize.org/v6/class/src/sequelize.js~Sequelize.html#instance-constructor-constructor) for options.
|
||||
```json
|
||||
"db": {
|
||||
|
@ -34,11 +34,15 @@ DB configuration, look [here](https://sequelize.org/v6/class/src/sequelize.js~Se
|
|||
"storage": "/tmp/db.sqlite"
|
||||
}
|
||||
```
|
||||
- ### Upload path
|
||||
Where to save images
|
||||
### Upload path
|
||||
Where to save images
|
||||
`"upload_path": "./uploads"`
|
||||
|
||||
- ### User locale
|
||||
### Plugins path
|
||||
Where to search for [plugins](/usage/plugins)
|
||||
`"plugins_path": "./plugins"`
|
||||
|
||||
### User locale
|
||||
Probably you want to modify some text for your specific community, that's
|
||||
why we thought the `user_locale` configuration: you can specify your version of
|
||||
each string of **gancio** making a directory with your locales inside.
|
||||
|
|
139
docs/install/nominatim.md
Normal file
139
docs/install/nominatim.md
Normal file
|
@ -0,0 +1,139 @@
|
|||
---
|
||||
layout: default
|
||||
title: Nominatim
|
||||
permalink: /install/nominatim
|
||||
parent: Install
|
||||
nav_order: 7
|
||||
---
|
||||
|
||||
## Nominatim installation
|
||||
{: .no_toc }
|
||||
|
||||
1. TOC
|
||||
{:toc}
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
For testing purposes you could skip the nominatim installation and use one of this geocoding providers that run a server for free:
|
||||
|
||||
- [https://photon.komoot.io/](https://photon.komoot.io/) [Terms of service](https://photon.komoot.io/)
|
||||
- [https://nominatim.openstreetmap.org/](https://nominatim.openstreetmap.org/) [Terms of service](https://operations.osmfoundation.org/policies/nominatim/)
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
From [https://nominatim.org/release-docs/latest/admin/Installation/](https://nominatim.org/release-docs/latest/admin/Installation/)
|
||||
|
||||
"A minimum of 2GB of RAM is required or installation will fail. For a full planet import 128GB of RAM or more are strongly recommended. Do not report out of memory problems if you have less than 64GB RAM."
|
||||
|
||||
### Planet mirrors
|
||||
There is a list of planet mirror at [https://wiki.openstreetmap.org/wiki/Planet.osm#Planet.osm_mirrors](https://wiki.openstreetmap.org/wiki/Planet.osm#Planet.osm_mirrors)
|
||||
There you can also find `Country and area extracts`, divided by `Worldwide extract sources` and `Regional extract sources`
|
||||
|
||||
### Download an extract
|
||||
For Nominatim to work, you will needs to import files in [PBF Format](https://wiki.openstreetmap.org/wiki/PBF_Format) in the PostGis database. Those files have extension `*.osm.pbf`.
|
||||
|
||||
Some of these mirrors provide also incremental updates via [OsmChange](https://wiki.openstreetmap.org/wiki/OsmChange), for example:
|
||||
- Provides updates but with a lower detail
|
||||
[https://download.geofabrik.de/europe/italy/nord-ovest-updates/nord-ovest-latest.osm.pbf](http://download.geofabrik.de/europe/italy/nord-ovest-latest.osm.pbf)
|
||||
[https://download.geofabrik.de/europe/italy/nord-ovest-updates/](https://download.geofabrik.de/europe/italy/nord-ovest-updates/)
|
||||
- Does not provide updates but as higher level of detail
|
||||
[https://osmit-estratti-test.wmcloud.org/dati/poly/province/pbf/015_Milano_poly.osm.pbf](https://osmit-estratti-test.wmcloud.org/dati/poly/province/pbf/015_Milano_poly.osm.pbf)
|
||||
|
||||
Needs to host multiple areas? Checkout [Osmium](https://osmcode.org/osmium-tool/manual.html), to merge multiple PBF files into one.
|
||||
|
||||
---
|
||||
|
||||
## Install on Debian
|
||||
There is a [detailed documentaion](https://nominatim.org/release-docs/latest/appendix/Install-on-Ubuntu-22/) for installing nominatim on `Ubuntu 22` that should be valid also to install on `Debian`.
|
||||
|
||||
### Setup
|
||||
[https://nominatim.org/release-docs/latest/appendix/Install-on-Ubuntu-22/#installing-the-required-software](https://nominatim.org/release-docs/latest/appendix/Install-on-Ubuntu-22/#installing-the-required-software)
|
||||
|
||||
### Building and Configuration
|
||||
|
||||
Get the source code from Github and change into the source directory
|
||||
```
|
||||
cd $USERHOME
|
||||
wget https://nominatim.org/release/Nominatim-4.2.0.tar.bz2
|
||||
tar xf Nominatim-4.2.0.tar.bz2
|
||||
```
|
||||
|
||||
The code must be built in a separate directory. Create this directory, then configure and build Nominatim in there:
|
||||
```
|
||||
mkdir $USERHOME/build
|
||||
cd $USERHOME/build
|
||||
cmake $USERHOME/Nominatim-4.2.0
|
||||
make
|
||||
```
|
||||
|
||||
### Setting up the webserver
|
||||
[https://nominatim.org/release-docs/latest/appendix/Install-on-Ubuntu-22/#setting-up-a-webserver](https://nominatim.org/release-docs/latest/appendix/Install-on-Ubuntu-22/#setting-up-a-webserver)
|
||||
|
||||
### Import the database
|
||||
[https://nominatim.org/release-docs/latest/admin/Import/](https://nominatim.org/release-docs/latest/admin/Import/)
|
||||
|
||||
---
|
||||
|
||||
## Install using docker
|
||||
|
||||
### Setup
|
||||
Make sure to have [Docker Engine](https://docs.docker.com/engine/install/),
|
||||
[Docker Compose](https://docs.docker.com/compose/install/) and [git](https://git-scm.com/downloads) installed:
|
||||
```bash
|
||||
sudo apt install docker docker-compose git
|
||||
```
|
||||
|
||||
### Clone the project
|
||||
From [https://github.com/mediagis/nominatim-docker](https://github.com/mediagis/nominatim-docker)
|
||||
|
||||
- Clone the project from sources
|
||||
```bash
|
||||
git clone git@github.com:mediagis/nominatim-docker.git
|
||||
# cd nominatim-docker/<version>
|
||||
cd nominatim-docker/4.2/contrib # released Nov 29, 2022
|
||||
docker-compose pull
|
||||
```
|
||||
|
||||
- Or, use the template at `docs/docker/nominatim`
|
||||
```
|
||||
cd /opt/gancio/docs/docker/nominatim
|
||||
docker-compose pull
|
||||
```
|
||||
|
||||
### Import the database
|
||||
See [Requirements](#requirements) about downloading the `.osm.pbf` files
|
||||
```bash
|
||||
cd docs/docker/nominatim/
|
||||
wget https://download.geofabrik.de/europe/italy/nord-ovest-updates/nord-ovest-latest.osm.pbf \
|
||||
./nominatim/data/default.osm.pbf
|
||||
```
|
||||
|
||||
### Configure the environment file
|
||||
```
|
||||
cd docs/docker/nominatim/
|
||||
cp .env.example .env
|
||||
```
|
||||
Create a random password for nominatim a add it to .env file
|
||||
```bash
|
||||
NOMINATIM_PASSWORD=random_password;
|
||||
NOMINATIM_PASSWORD=$(echo $NOMINATIM_PASSWORD | openssl passwd --stdin);
|
||||
echo $NOMINATIM_PASSWORD;
|
||||
sed -i -e 's/\(NOMINATIM_PASSWORD=\)\(.*\)/\1'$NOMINATIM_PASSWORD'/g' .env
|
||||
```
|
||||
|
||||
### Start nominatim-docker
|
||||
|
||||
Start your container:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
Checkout the logs to see when data are imported to the database:
|
||||
```bash
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
Try out the search:
|
||||
|
||||
[http://0.0.0.0:8080/search?q=building](http://0.0.0.0:8080/search?q=building)
|
|
@ -11,14 +11,20 @@ nav_order: 7
|
|||
- [sapratza.in](https://sapratza.in/) (Sardinia, Italy)
|
||||
- [ponente.rocks](https://ponente.rocks) (Ponente Ligure, Italy)
|
||||
- [puntello.org](https://puntello.org) (Milan, Italy)
|
||||
- [lasitua.org](https://lasitua.org) (Brescia, Italy)
|
||||
- [balotta.org](https://balotta.org) (Bologna, Italy)
|
||||
- [gancio.daghe.xyz](https://gancio.daghe.xyz/) (Trento, Italy)
|
||||
- [bcn.convoca.la](https://bcn.convoca.la/) (Barcelona)
|
||||
- [mad.convoca.la](https://bcn.convoca.la/) (Madrid)
|
||||
- [bonn.jetzt](https://bonn.jetzt/) (Digital-Events aus Bonn, Rhein-Sieg und der Region)
|
||||
- [quest.livellosegreto.it](https://quest.livellosegreto.it/)
|
||||
- [ezkerraldea.euskaragendak.eus](https://ezkerraldea.euskaragendak.eus/)
|
||||
- [lakelogaztetxea.net](https://lakelogaztetxea.net)
|
||||
- [agenda.eskoria.eus](https://agenda.eskoria.eus/)
|
||||
- [lubakiagenda.net](https://lubakiagenda.net/)
|
||||
|
||||
- [eventos.coletivos.org](https://eventos.coletivos.org/)
|
||||
- [calendario.extinctionrebellion.es](https://calendario.extinctionrebellion.es/)
|
||||
- [cloudspeakers.org](https://cloudspeakers.org/) (Utrecht?)
|
||||
|
||||
|
||||
<small>Do you want your instance to appear here? [Write us]({% link contact.md %}).</small>
|
||||
|
|
62
docs/usage/plugins.md
Normal file
62
docs/usage/plugins.md
Normal file
|
@ -0,0 +1,62 @@
|
|||
---
|
||||
layout: default
|
||||
title: Plugins
|
||||
permalink: /usage/plugins
|
||||
nav_order: 2
|
||||
parent: Usage
|
||||
has_toc: true
|
||||
---
|
||||
|
||||
# Plugins
|
||||
{: .no_toc }
|
||||
|
||||
This page is a guide to install plugins, if you want to develop one instead look [here](/dev/plugins)
|
||||
|
||||
1. TOC
|
||||
{:toc}
|
||||
|
||||
|
||||
|
||||
## Install
|
||||
|
||||
To install a plugin you have to:
|
||||
|
||||
1. **download the .zip archive (look for the url on the plugin list below)**
|
||||
```
|
||||
wget https://framagit.org/les/gancio-plugin-telegram-bridge/-/archive/v0.2.0/gancio-plugin-telegram-bridge-v0.2.0.zip
|
||||
```
|
||||
|
||||
2. **unpack it in the `./plugins` directory.**
|
||||
```
|
||||
cd plugins
|
||||
unzip https://framagit.org/les/gancio-plugin-telegram-bridge/-/archive/v0.2.0/gancio-plugin-telegram-bridge-v0.2.0.zip
|
||||
```
|
||||
|
||||
|
||||
3. **install the dependencies with `yarn`**
|
||||
```
|
||||
cd plugins/gancio-plugin-telegram-bridge
|
||||
yarn
|
||||
```
|
||||
|
||||
4. **restart gancio**
|
||||
__with debian__
|
||||
```
|
||||
sudo sytemctl restart gancio
|
||||
```
|
||||
__with docker__
|
||||
```
|
||||
docker-compose restart
|
||||
```
|
||||
|
||||
# List of plugins
|
||||
|
||||
## __Telegram__
|
||||
|
||||
This plugin republishes events to Telegram channels or groups.
|
||||
The goal is to spread the info of our networks to the capitalist cyberspace, and pull otherwise isolated people to our radical and free part of the internet.
|
||||
|
||||
- **Website**: [https://framagit.org/bcn.convocala/gancio-plugin-telegram-bridge](https://framagit.org/bcn.convocala/gancio-plugin-telegram-bridge)
|
||||
- **Download**: [gancio-plugin-telegram-bridge-v0.2.0.zip](https://framagit.org/les/gancio-plugin-telegram-bridge/-/archive/v0.2.0/gancio-plugin-telegram-bridge-v0.2.0.zip)
|
||||
- **Release**: v0.2.0 / 10 Dec '22
|
||||
|
1
gancio_plugins/gancio-plugin-telegram-bridge
Submodule
1
gancio_plugins/gancio-plugin-telegram-bridge
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 0486d0852db569b7064535f0d52709e06e9ecd1f
|
|
@ -1,18 +1,10 @@
|
|||
<template>
|
||||
<v-app app>
|
||||
<Snackbar/>
|
||||
<Confirm/>
|
||||
<Nav/>
|
||||
<v-main app>
|
||||
<v-container fluid class='pa-0'>
|
||||
<div v-if='showCollections || showBack'>
|
||||
<v-btn class='ml-2 mt-2' v-if='showBack' outlined color='primary' to='/'><v-icon v-text='mdiChevronLeft'></v-icon></v-btn>
|
||||
<v-btn class='ml-2 mt-2' outlined v-for='collection in collections' color='primary' :key='collection.id' :to='`/collection/${collection.name}`'>{{collection.name}}</v-btn>
|
||||
</div>
|
||||
<v-fade-transition hide-on-leave>
|
||||
<nuxt />
|
||||
</v-fade-transition>
|
||||
</v-container>
|
||||
<v-app>
|
||||
<Appbar/>
|
||||
<v-main>
|
||||
<Snackbar/>
|
||||
<Confirm/>
|
||||
<nuxt :keep-alive='$route.name === "index"'/>
|
||||
</v-main>
|
||||
<Footer/>
|
||||
|
||||
|
@ -21,12 +13,11 @@
|
|||
|
||||
</template>
|
||||
<script>
|
||||
import Nav from '~/components/Nav.vue'
|
||||
import Appbar from '../components/Appbar.vue'
|
||||
import Snackbar from '../components/Snackbar'
|
||||
import Footer from '../components/Footer'
|
||||
import Confirm from '../components/Confirm'
|
||||
import { mapState } from 'vuex'
|
||||
import { mdiChevronLeft } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
head () {
|
||||
|
@ -37,29 +28,9 @@ export default {
|
|||
link: [{ rel: 'icon', type: 'image/png', href: this.settings.baseurl + '/logo.png' }],
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return { collections: [], mdiChevronLeft }
|
||||
},
|
||||
async fetch () {
|
||||
if (this.$route.name && ['tag-tag', 'index', 'g-collection', 'p-place'].includes(this.$route.name)) {
|
||||
this.collections = await this.$axios.$get('collections').catch(_e => [])
|
||||
} else {
|
||||
this.collections = []
|
||||
}
|
||||
|
||||
},
|
||||
name: 'Default',
|
||||
components: { Nav, Snackbar, Footer, Confirm },
|
||||
computed: {
|
||||
...mapState(['settings', 'locale']),
|
||||
showBack () {
|
||||
return ['tag-tag', 'collection-collection', 'place-place', 'search', 'announcement-id'].includes(this.$route.name)
|
||||
},
|
||||
showCollections () {
|
||||
if (!this.collections || this.collections.length === 0) return false
|
||||
return ['tag-tag', 'index', 'g-collection', 'p-place'].includes(this.$route.name)
|
||||
}
|
||||
},
|
||||
components: { Appbar, Snackbar, Footer, Confirm },
|
||||
computed: mapState(['settings']),
|
||||
created () {
|
||||
this.$vuetify.theme.dark = this.settings['theme.is_dark']
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
v-app(app)
|
||||
Snackbar
|
||||
Confirm
|
||||
Nav
|
||||
Appbar
|
||||
|
||||
v-main(app)
|
||||
v-fade-transition(hide-on-leave)
|
||||
|
@ -12,13 +12,13 @@ v-app(app)
|
|||
|
||||
</template>
|
||||
<script>
|
||||
import Nav from '~/components/Nav.vue'
|
||||
import Appbar from '~/components/Appbar.vue'
|
||||
import Snackbar from '../components/Snackbar'
|
||||
import Footer from '../components/Footer'
|
||||
import Confirm from '../components/Confirm'
|
||||
|
||||
export default {
|
||||
name: 'Default',
|
||||
components: { Nav, Snackbar, Footer, Confirm }
|
||||
components: { Appbar, Snackbar, Footer, Confirm }
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -89,7 +89,19 @@
|
|||
"max_events": "Nre. màx. d'activitats",
|
||||
"close": "Tanca",
|
||||
"blobs": "Blobs",
|
||||
"collections": "Coŀleccions"
|
||||
"collections": "Coŀleccions",
|
||||
"show_map": "Mostra el mapa",
|
||||
"latitude": "Latitud",
|
||||
"longitude": "Longitud",
|
||||
"getting_there": "Com arribar",
|
||||
"plugins": "Complements",
|
||||
"home": "Inici",
|
||||
"help_translate": "Ajuda amb la traducció",
|
||||
"calendar": "Calendari",
|
||||
"about": "Sobre aquesta agenda",
|
||||
"content": "Contingut",
|
||||
"admin_actions": "Accions d'administració",
|
||||
"recurring_event_actions": "Accions d'activitats recorrents"
|
||||
},
|
||||
"login": {
|
||||
"description": "Amb la sessió iniciada pots afegir activitats noves.",
|
||||
|
@ -128,6 +140,8 @@
|
|||
"added": "S'ha afegit l'activitat",
|
||||
"added_anon": "S'ha afegit l'activitat però encara ha de ser confirmada.",
|
||||
"where_description": "On es farà? Si no està posat, escriu-ho i prem Enter.",
|
||||
"coordinates_search": "Cerca de coordenades",
|
||||
"coordinates_search_description": "Podeu cercar el lloc pel nom o enganxar el parell de coordenades",
|
||||
"confirmed": "S'ha confirmat l'activitat",
|
||||
"not_found": "No s'ha trobat l'activitat",
|
||||
"remove_confirmation": "Segur que vols esborrar l'activitat?",
|
||||
|
@ -142,10 +156,10 @@
|
|||
"normal_description": "Tria el dia.",
|
||||
"recurrent_1w_days": "Cada {days}",
|
||||
"recurrent_2w_days": "Un de cada dos {days}",
|
||||
"recurrent_1m_days": "|El dia {days} de cada mes|Els dies {days} de cada mes",
|
||||
"recurrent_2m_days": "|El dia {days} d'un de cada dos mesos|Els dies {days} d'un de cada dos mesos",
|
||||
"recurrent_1m_days": "El dia {days} de cada mes",
|
||||
"recurrent_2m_days": "El dia {days} d'un de cada dos mesos",
|
||||
"recurrent_1m_ordinal": "El {n} {days} de cada mes",
|
||||
"recurrent_2m_ordinal": "|El {n} {days} de cada dos mesos|Els {n} {days} de cada dos mesos",
|
||||
"recurrent_2m_ordinal": "El {n} {days} de cada dos mesos",
|
||||
"each_week": "Cada setmana",
|
||||
"each_2w": "Cada dues setmanes",
|
||||
"each_month": "Cada mes",
|
||||
|
@ -153,7 +167,7 @@
|
|||
"from": "Des de les",
|
||||
"image_too_big": "La imatge és massa gran! Max 4 MB",
|
||||
"interact_with_me_at": "Interacciona amb mi a",
|
||||
"follow_me_description": "Entre les diverses maneres d'estar al dia de les activitats que es publiquen aquí a {title},\n pots seguir-nos al compte <u>{account}</u> des de Mastodon o altres, i afegir recursos des d'allà. <br/> <br/>\nSi no has sentit mai sobre «Mastodon» o «Fedivers», recomanem fer un cop d'ull a <a href='https://equipamentslliures.cat/divulgacio/fediverse'>aquesta breu introducció al Fedivers</a>. <br/> <br/> Introdueix la teva instància a sota (ex: kolektiva.social o mastodont.cat)",
|
||||
"follow_me_description": "Entre les diverses maneres d'estar al dia de les activitats que es publiquen aquí a {title},\npots seguir-nos al compte <u>{account}</u> des de Mastodon o altres, i afegir recursos des d'allà. <br/> <br/>\nSi no has sentit mai sobre «Mastodon» o «Fedivers», recomanem fer un cop d'ull a <a href='https://framatube.org/w/9dRFC6Ya11NCVeYKn8ZhiD'>aquest vídeo introductori</a> i llegir-ne més a <a href='https://fedi.cat/fediverse'>aquest portal en català</a>. <br/> <br/> Introdueix la teva instància a sota (ex: kolektiva.social, mastodont.cat, ...)",
|
||||
"interact_with_me": "Segueix-nos al fedivers",
|
||||
"remove_recurrent_confirmation": "Estàs segur/a d'esborrar aquesta activitat periòdica?\nNo s'esborraran les ocurrències antigues, només es deixaran de crear les futures.",
|
||||
"ics": "ICS",
|
||||
|
@ -164,9 +178,11 @@
|
|||
"saved": "S'ha desat l'activitat",
|
||||
"import_description": "Pots importar activitats des d'altres instàncies o plataformes que facin servir formats estàndards (ics o h-event)",
|
||||
"remove_media_confirmation": "Confirmeu l'eliminació de la imatge?",
|
||||
"download_flyer": "Baixa el flyer",
|
||||
"download_flyer": "Baixa el cartell",
|
||||
"alt_text_description": "Descripció per a persones amb discapacitat visual",
|
||||
"choose_focal_point": "Tria el punt focal"
|
||||
"choose_focal_point": "Tria el punt focal",
|
||||
"address_description": "Quina és l'adreça completa del lloc?",
|
||||
"address_description_osm": "Quina és l'adreça completa del lloc? (gràcies a la comunitat d'<a href='http://osm.org/copyright'>OpenStreetMap</a>)"
|
||||
},
|
||||
"admin": {
|
||||
"place_description": "En el cas que un lloc és incorrecte o l'adreça ha de canviar, pots arreglar-ho.<br/>Tingues en compte que totes les activitats passades i futures associades amb aquest lloc també canviaran d'adreça.",
|
||||
|
@ -179,6 +195,7 @@
|
|||
"allow_registration_description": "Vols deixar el registre obert?",
|
||||
"allow_anon_event": "Vols permetre activitats anònimes (s'han de confirmar manualment)?",
|
||||
"allow_recurrent_event": "Habilitar activitats periòdiques",
|
||||
"allow_geolocation": "Habilitar la geolocalització d' esdeveniments",
|
||||
"recurrent_event_visible": "Mostra per defecte les activitats periòdiques",
|
||||
"federation": "Federació / ActivityPub",
|
||||
"enable_federation": "Activa la federació",
|
||||
|
@ -215,6 +232,9 @@
|
|||
"instance_name_help": "Nom del compte ActivityPub per a seguir",
|
||||
"enable_trusted_instances": "Mostra les instàncies amigues",
|
||||
"trusted_instances_help": "Les instàncies amigues apareixen en la capçalera, a la barra de navegació superior",
|
||||
"trusted_instances_label": "Etiqueta de navegació a instàncies amigues",
|
||||
"trusted_instances_label_default": "Instàncies amigues",
|
||||
"trusted_instances_label_help": "L'etiqueta per defecte és 'Instàncies amigues'",
|
||||
"add_trusted_instance": "Afegeix una instància amiga",
|
||||
"instance_place_help": "L'etiqueta per mostrar al menú de les altres instàncies amigues",
|
||||
"delete_trusted_instance_confirm": "Segur que vols eliminar aquest element del menú d'instàncies amigues?",
|
||||
|
@ -239,7 +259,7 @@
|
|||
"new_blob": "Nou blob",
|
||||
"blobs_description": "Els blobs són agrupacions d'activitats per etiquetes i llocs. Es mostren a la pàgina principal",
|
||||
"edit_blob": "Edita el blob",
|
||||
"enable_admin_user_confirm": "N'estàs segur que vols donar drets d'administrador a {user}?",
|
||||
"enable_admin_user_confirm": "N'estàs segur/a de donar drets d'administració a {user}?",
|
||||
"smtp_port": "Port SMTP",
|
||||
"smtp_use_sendmail": "Utilitza sendmail",
|
||||
"new_collection": "Col·lecció nova",
|
||||
|
@ -247,7 +267,18 @@
|
|||
"disable_admin_user_confirm": "N'estàs segur de suprimir els permisos d'administrador de {usuari}?",
|
||||
"collections_description": "Les col·leccions són agrupacions d'esdeveniments per etiquetes i llocs. Es mostraran a la pàgina d'inici",
|
||||
"smtp_secure": "SMTP segur (TLS o STARTTLS)",
|
||||
"sender_email": "Correu remitent"
|
||||
"sender_email": "Correu remitent",
|
||||
"hide_thumbs": "Amaga les miniatures dels cartells",
|
||||
"hide_calendar": "Amaga el calendari",
|
||||
"fallback_image": "Cartell per defecte",
|
||||
"config_plugin": "Configura el complement",
|
||||
"header_image": "Imatge de capçalera",
|
||||
"default_images": "Cartell per defecte",
|
||||
"blocked": "Bloquejat/da",
|
||||
"domain": "Domini",
|
||||
"known_users": "Usuàries conegudes",
|
||||
"created_at": "Creada",
|
||||
"default_images_help": "<a href='/admin?tab=theme'>Actualitza la pàgina</a> per veure els canvis."
|
||||
},
|
||||
"auth": {
|
||||
"not_confirmed": "Encara no s'ha confirmat…",
|
||||
|
|
|
@ -88,7 +88,19 @@
|
|||
"associate": "Partner:in",
|
||||
"collections": "Sammlungen",
|
||||
"max_events": "Maximale Anzahl an Veranstaltungen",
|
||||
"close": "Schließe"
|
||||
"close": "Schließe",
|
||||
"show_map": "Karte anzeigen",
|
||||
"latitude": "Breite",
|
||||
"longitude": "Länge",
|
||||
"getting_there": "Anreise",
|
||||
"calendar": "Kalender",
|
||||
"home": "Startseite",
|
||||
"about": "Über",
|
||||
"plugins": "Plugins",
|
||||
"help_translate": "Hilf beim Übersetzen mit",
|
||||
"content": "Inhalt",
|
||||
"admin_actions": "Aktionen der Administrierenden",
|
||||
"recurring_event_actions": "Einstellungen für regelmäßige Veranstaltungen"
|
||||
},
|
||||
"admin": {
|
||||
"delete_footer_link_confirm": "Möchtest du diesen Link löschen?",
|
||||
|
@ -120,8 +132,11 @@
|
|||
"wrong_domain_warning": "Die \"baseurl\" die in config.json konfiguriert ist <b>({baseurl})</b> unterscheidet sich von derjenigen <b>({url})</b> die du besuchst",
|
||||
"instance_place_help": "Diese Textzeile wird im Menü der anderen befreundeten Instanzen angezeigt",
|
||||
"place_description": "Falls ein Ort falsch ist oder sich die Adresse ändert, kannst du ihn ändern.<br/>Bitte beachte, dass alle Veranstaltungen, die mit diesem Ort verbunden sind, die Adresse ändern (auch zurückliegende).",
|
||||
"enable_admin_user_confirm": "Achte darauf, dass du der nutzenden Person {user} Admin-Rechte hinzufügst",
|
||||
"enable_admin_user_confirm": "Bist du dir sicher, dass du der nutzenden Person {user} Admin-Rechte gewährst?",
|
||||
"trusted_instances_help": "Befreundete Instanzen werden in der Navigationsleiste oben auf der Seite angezeigt",
|
||||
"trusted_instances_label": "Navigationsbezeichnung zu Friend-Instanzen",
|
||||
"trusted_instances_label_default": "Freundliche Instanzen",
|
||||
"trusted_instances_label_help": "Die Standardbezeichnung ist 'Freundliche Instanzen'",
|
||||
"delete_announcement_confirm": "Möchtest du diesen Hinweis entfernen?",
|
||||
"instance_timezone_description": "Gancio wurde dazu entwickelt, um die Veranstaltungen eines bestimmten Ortes, z. B. einer Stadt, zu erfassen. Wenn du die Zeitzone dieses Ortes eingibst und auswählst, werden alle Zeiten angezeigt und wie ausgewählt eingegeben.",
|
||||
"instance_locale_description": "Die Sprache, in der die Seiten angezeigt werden, ist die von der nutzenden Person bevorzugte Sprache. In einigen Fällen ist es jedoch erforderlich, die Meldungen für alle auf dieselbe Weise anzuzeigen (z. B. bei der Veröffentlichung über ActivityPub oder beim Versand bestimmter E-Mails). In diesen Fällen wird die oben gewählte Sprache verwendet.",
|
||||
|
@ -155,6 +170,7 @@
|
|||
"enable_federation": "Federation aktivieren",
|
||||
"allow_anon_event": "Kann man auch anonyme Veranstaltungen (vorausgesetzt, diese werden genehmigt) eintragen?",
|
||||
"allow_registration_description": "Möchtest du die Registrierung aktivieren?",
|
||||
"allow_geolocation": "Aktivieren der Ereignis-Geolokalisierung",
|
||||
"federation": "Föderation / ActivityPub",
|
||||
"enable_federation_help": "Bei Aktivierung kann diese Instanz vom Fediverse aus verfolgt werden",
|
||||
"add_instance": "Instanz hinzufügen",
|
||||
|
@ -165,7 +181,38 @@
|
|||
"smtp_hostname": "SMTP-Hostname",
|
||||
"smtp_description": "<ul><li>Der Admin erhält eine E-Mail, wenn ein anonymes Ereignis hinzugefügt wird (falls aktiviert).</li><li>Der Admin erhält E-Mails mit der Aufforderung zur Registrierung (falls aktiviert).</li><li>Die nutzenden Personen erhalten eine E-Mail zur Bestätigung der angeforderten Registrierung.</li><li>Die nutzenden Personen erhalten eine E-Mail mit der Anmeldebestätigung.</li><li>Nutzende Personen erhalten eine E-Mail-Benachrichtigung, wenn sie direkt vom Admin registriert werden.</li><li>Nutzende Personen erhalten eine E-Mail, um ihr Passwort zu ändern, wenn sie es vergessen haben</li></ul>",
|
||||
"smtp_test_button": "Versenden einer Test-E-Mail",
|
||||
"smtp_test_success": "Eine Test-E-Mail wurde an {admin_email} gesendet, überprüfe deine Mailbox"
|
||||
"smtp_test_success": "Eine Test-E-Mail wurde an {admin_email} gesendet, überprüfe deine Mailbox",
|
||||
"fallback_image": "Ersatzbild",
|
||||
"header_image": "Bild der Überschrift",
|
||||
"default_images": "Standard Bilder",
|
||||
"config_plugin": "Plugin Konfiguration",
|
||||
"hide_thumbs": "Vorschaubilder ausblenden",
|
||||
"hide_calendar": "Kalender verstecken",
|
||||
"admin_email_help": "Die Adresse, die wir als Absender für den Versand von E-Mails verwenden. Sie ist auch die Adresse, an die deine E-Mails an die Administrator:innen geschickt werden.",
|
||||
"blocked": "Geblockt",
|
||||
"domain": "Domain",
|
||||
"known_users": "Bekannte Nutzer:innen",
|
||||
"created_at": "Erstellt am",
|
||||
"geocoding_provider_type": "Software für Georeferenzierung",
|
||||
"geocoding_provider_type_help": "Die Standard-Software ist Nominatim",
|
||||
"geocoding_provider": "Anbieter von Geocodierung",
|
||||
"geocoding_provider_help": "Der Standard-Anbieter ist Nominatim",
|
||||
"geocoding_countrycodes": "Gebietskennziffern",
|
||||
"geocoding_countrycodes_help": "Ermöglicht die Einrichtung eines Filters für die Suche auf der Grundlage von Ländercodes",
|
||||
"geocoding_test_button": "Geokodierung testen",
|
||||
"geocoding_test_success": "Der Geokodierdienst unter {service_name} funktioniert",
|
||||
"geocoding_test_error": "Der Dienst ist unter der angegebenen Adresse nicht zu erreichen: {service_name}",
|
||||
"tilelayer_provider": "Kachel-LayerAnbieter",
|
||||
"tilelayer_provider_help": "Der Standard-Anbieter ist OpenStreetMap",
|
||||
"tilelayer_provider_attribution": "Namensnennung",
|
||||
"tilelayer_test_button": "Kachel-Layer testen",
|
||||
"tilelayer_test_success": "Der Kachel-Layer-Dienst unter {service_name} funktioniert",
|
||||
"tilelayer_test_error": "Der Dienst ist unter der angegebenen Adresse nicht zu erreichen: {service_name}",
|
||||
"geolocation": "Geolokation",
|
||||
"allow_multidate_event": "Lasse mehrtägige Veranstaltungen zu",
|
||||
"admin_email": "E-Mail von der administrierenden Person",
|
||||
"geolocation_description": "<b>1. Bestimme einen Anbieter für einen Geokodierdienst</b>.<br>Derzeit gibt es unter den im <a href=\"https://wiki.openstreetmap.org/wiki/Nominatim#Alternatives_.2F_Third-party_providers\">Wiki von OpenStreetMap</a>, Anbietern Unterstützung für die Software <a href=\"https://github.com/osm-search/Nominatim\">Nominatim</a> und <a href=\"https://github.com/komoot/photon\">Photon</a>.<br>Du kannst eine der entsprechenden offiziellen Demos verwenden, indem du den Link in das Feld \"Geocoding provider\" kopierst:<ul><li>https://nominatim.openstreetmap.org/search (<a href=\"https://operations.osmfoundation.org/policies/nominatim/\">Terms of Service</a>)</li><li>https://photon.komoot.io/api/ (<a href=\"https://photon.komoot.io/\">Terms of Service</a>)</li></ul><br><b>2. Definiere einen Anbieter für Kartenebenen.</b><br>Eine Liste von ihnen findest du hier: <a href=\"https://leaflet-extras.github.io/leaflet-providers/preview/\">https://leaflet-extras.github.io/leaflet-providers/preview/</a>",
|
||||
"default_images_help": "Du musst <a href='/admin?tab=theme'>die Seite neu laden</a>, um die Änderungen sehen zu können."
|
||||
},
|
||||
"settings": {
|
||||
"update_confirm": "Willst du deine Änderung speichern?",
|
||||
|
@ -229,10 +276,12 @@
|
|||
"remove_recurrent_confirmation": "Bist du dir sicher, dass du diese wiederkehrende Veranstaltung entfernen möchtest?\nFrühere Veranstaltungen werden beibehalten, aber es werden keine zukünftigen Veranstaltungen mehr hinzugefügt.",
|
||||
"anon_description": "Du kannst eine Veranstaltung hinzufügen, ohne dich zu registrieren oder anzumelden,\nmusst dann aber warten, bis jemand es liest und bestätigt, dass es eine zulässige Veranstaltung ist.\nEs ist nicht möglich, den Eintrag zu verändern.<br/><br/>\nDu kannst dich stattdessen <a href='/login'>anmelden</a> oder <a href='/register'>registrieren</a>. In diesem Fall solltest du so schnell wie möglich eine Antwort erhalten. ",
|
||||
"where_description": "Wo ist die Veranstaltung? Wenn der Ort noch nicht beschrieben wurde, kannst du ihn selbst eintragen.",
|
||||
"coordinates_search": "Suche nach Koordinaten",
|
||||
"coordinates_search_description": "Sie können den Ort anhand des Namens suchen oder das Koordinatenpaar einfügen.",
|
||||
"follow_me_description": "Eine Möglichkeit, über die hier auf {title} veröffentlichten Veranstaltungen auf dem Laufenden zu bleiben, besteht darin, dem Account <u>{account}</u> aus dem Fediverse zu folgen, zum Beispiel über Mastodon, und von dort aus eventuell Ressourcen für eine Veranstaltung hinzuzufügen.<br/><br/>\nWenn Du noch nie von Mastodon und dem Fediverse gehört hast, empfehlen wir dir, diesen Artikel <a href='https://www.savjee.be/videos/simply-explained/mastodon-and-fediverse-explained/'> zu lesen</a>.<br/><br/>Gib unten deine Instanz ein (z.B. mastodon.social)",
|
||||
"media_description": "Du kannst (optional) einen Flyer hinzufügen",
|
||||
"edit_recurrent": "Bearbeite eine sich wiederholende Veranstaltung :",
|
||||
"show_recurrent": "regelmäßige Veranstaltungen",
|
||||
"show_recurrent": "wiederkehrende Termine",
|
||||
"show_past": "auch ältere Veranstaltungen",
|
||||
"only_future": "nur zukünftige Veranstaltungen",
|
||||
"recurrent_description": "Wähle die Häufigkeit und Tage aus",
|
||||
|
@ -242,11 +291,11 @@
|
|||
"normal_description": "Wähle den Tag aus.",
|
||||
"recurrent_1w_days": "Jeden {days}",
|
||||
"recurrent_2w_days": "Jeden zweiten {days} eine",
|
||||
"recurrent_1m_days": "|Der {days} in jedem Monat|{days} in jedem Monat",
|
||||
"recurrent_2m_days": "Jeder {days} im Monat",
|
||||
"recurrent_1m_days": "Der {days} in jedem Monat",
|
||||
"recurrent_2m_days": "Der {days} jeden zweiten Monat",
|
||||
"recurrent_1m_ordinal": "Jeden {n} {days} im Monat",
|
||||
"each_week": "Jede Woche",
|
||||
"recurrent_2m_ordinal": "Jeden {n} {days} im Monat",
|
||||
"recurrent_2m_ordinal": "Der {n} {days} jeden zweiten Monat",
|
||||
"each_month": "Jeden Monat",
|
||||
"due": "bis",
|
||||
"from": "von",
|
||||
|
@ -256,7 +305,9 @@
|
|||
"download_flyer": "Lade den Flyer herunter",
|
||||
"remove_media_confirmation": "Möchtest du das Bild löschen?",
|
||||
"alt_text_description": "Beschreibung für Menschen mit Sehbehinderungen",
|
||||
"choose_focal_point": "Auswahl des Schwerpunkts"
|
||||
"choose_focal_point": "Auswahl des Schwerpunkts",
|
||||
"address_description": "Was ist die Adresse?",
|
||||
"address_description_osm": "Wo ist die Adresse? (<a href='http://osm.org/copyright'>OpenStreetMap</a> contributors)"
|
||||
},
|
||||
"register": {
|
||||
"first_user": "Admin erstellt",
|
||||
|
|
|
@ -18,5 +18,11 @@
|
|||
"admin_register": {
|
||||
"subject": "Nowa rejestracja",
|
||||
"content": "{{user.email}} poprosił(a) o rejestrację na {{config.title}}: <br/><pre>{{user.description}}</pre><br/> Zatwierdź ją <a href='{{config.baseurl}}/admin'>tutaj</a>."
|
||||
},
|
||||
"event_confirm": {
|
||||
"content": "Żeby potwierdzić to wydarzenie wejdź w <a href='{{url}}'>ten link</a>"
|
||||
},
|
||||
"test": {
|
||||
"subject": "Serwer SMTP skonfigurowany prawidłowo"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1,29 @@
|
|||
{}
|
||||
{
|
||||
"register": {
|
||||
"subject": "Pedido de registo recebido",
|
||||
"content": "Recebemos o pedido de registo. Vamos confirmar assim que possível."
|
||||
},
|
||||
"confirm": {
|
||||
"content": "Olá, sua conta em <a href='{{config.baseurl}}'>{{config.title}}</a> foi confirmada. Escreva para {{config.admin_email}} para obter qualquer informação.",
|
||||
"subject": "Você pode agora começar a publicar eventos"
|
||||
},
|
||||
"event_confirm": {
|
||||
"content": "Você pode confirmar este evento <a href='{{url}}'>nesta página</a>"
|
||||
},
|
||||
"test": {
|
||||
"subject": "Sua configuração SMTP está funcionando",
|
||||
"content": "Este é um e-mail de teste, se estiver lendo isto, sua configuração está funcionando."
|
||||
},
|
||||
"user_confirm": {
|
||||
"subject": "Você pode agora começar a publicar eventos",
|
||||
"content": "Olá, sua conta em <a href='{{config.baseurl}}'>{{config.title}}</a> foi criada. <a href='{{config.baseurl}}/user_confirm/{{user.recover_code}}'>Confirme e escolha uma senha.</a>."
|
||||
},
|
||||
"admin_register": {
|
||||
"subject": "Novo registro",
|
||||
"content": "{{user.email}} pediu o registro em {{config.title}}: <br/><pre>{{user.description}}</pre><br/> Confirme <a href='{{config.baseurl}}/admin'>aqui</a>."
|
||||
},
|
||||
"recover": {
|
||||
"subject": "Recuperação de senha",
|
||||
"content": "Olá, você solicitou a recuperação de sua senha em {{config.title}}. <a href='{{config.baseurl}}/recover/{{user.recover_code}}'>Clique aqui</a> para confirmar."
|
||||
}
|
||||
}
|
||||
|
|
1
locales/email/zh.json
Normal file
1
locales/email/zh.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
634
locales/en.json
634
locales/en.json
|
@ -1,295 +1,345 @@
|
|||
{
|
||||
"common": {
|
||||
"add_event": "Add event",
|
||||
"next": "Next",
|
||||
"export": "Export",
|
||||
"send": "Send",
|
||||
"where": "Where",
|
||||
"address": "Address",
|
||||
"when": "When",
|
||||
"what": "What",
|
||||
"media": "Media",
|
||||
"login": "Login",
|
||||
"email": "E-mail",
|
||||
"password": "Password",
|
||||
"register": "Register",
|
||||
"description": "Description",
|
||||
"remove": "Remove",
|
||||
"hide": "Hide",
|
||||
"search": "Search",
|
||||
"edit": "Edit",
|
||||
"info": "Info",
|
||||
"confirm": "Confirm",
|
||||
"admin": "Admin",
|
||||
"users": "Users",
|
||||
"events": "Events",
|
||||
"places": "Places",
|
||||
"settings": "Options",
|
||||
"actions": "Actions",
|
||||
"deactivate": "Turn off",
|
||||
"remove_admin": "Remove admin",
|
||||
"activate": "Activate",
|
||||
"save": "Save",
|
||||
"preview": "Preview",
|
||||
"logout": "Log out",
|
||||
"share": "Share",
|
||||
"name": "Name",
|
||||
"associate": "Associate",
|
||||
"edit_event": "Edit event",
|
||||
"related": "Related",
|
||||
"add": "Add",
|
||||
"logout_ok": "Logged out",
|
||||
"copy": "Copy",
|
||||
"recover_password": "Recover password",
|
||||
"new_password": "New password",
|
||||
"new_user": "New user",
|
||||
"ok": "Ok",
|
||||
"cancel": "Cancel",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable",
|
||||
"me": "You",
|
||||
"password_updated": "Password changed.",
|
||||
"resources": "Resources",
|
||||
"n_resources": "no resource|a resource|{n} resources",
|
||||
"activate_user": "Confirmed",
|
||||
"displayname": "Display name",
|
||||
"federation": "Federation",
|
||||
"set_password": "Set password",
|
||||
"copy_link": "Copy link",
|
||||
"send_via_mail": "Send e-mail",
|
||||
"add_to_calendar": "Add to calendar",
|
||||
"instances": "Instances",
|
||||
"copied": "Copied",
|
||||
"embed": "Embed",
|
||||
"embed_title": "Embed this event on your website",
|
||||
"embed_help": "Copying the following code into your website and the event will be shown like here",
|
||||
"feed": "RSS Feed",
|
||||
"feed_url_copied": "Open the copied feed URL in your RSS feed reader",
|
||||
"follow_me_title": "Follow updates from fediverse",
|
||||
"follow": "Follow",
|
||||
"moderation": "Moderation",
|
||||
"user": "User",
|
||||
"authorize": "Authorize",
|
||||
"title": "Title",
|
||||
"filter": "Filter",
|
||||
"event": "Event",
|
||||
"pause": "Pause",
|
||||
"start": "Start",
|
||||
"fediverse": "Fediverse",
|
||||
"skip": "Skip",
|
||||
"delete": "Remove",
|
||||
"announcements": "Announcements",
|
||||
"url": "URL",
|
||||
"place": "Place",
|
||||
"tags": "Tags",
|
||||
"theme": "Theme",
|
||||
"reset": "Reset",
|
||||
"import": "Import",
|
||||
"max_events": "N. max events",
|
||||
"label": "Label",
|
||||
"collections": "Collections",
|
||||
"close": "Close"
|
||||
},
|
||||
"login": {
|
||||
"description": "By logging in you can publish new events.",
|
||||
"check_email": "Check your e-mail inbox and spam.",
|
||||
"not_registered": "Not registered?",
|
||||
"forgot_password": "Forgot your password?",
|
||||
"error": "Could not log in. Check your login info.",
|
||||
"insert_email": "Enter your e-mail address",
|
||||
"ok": "Logged in"
|
||||
},
|
||||
"recover": {
|
||||
"not_valid_code": "Something went wrong."
|
||||
},
|
||||
"export": {
|
||||
"intro": "Unlike the unsocial platforms that do everything to keep users and data about them, we believe that information, like people, must be free. For this you can stay updated on the events you want, without necessarily going through this site.",
|
||||
"email_description": "You can get events that interest sent via e-mail.",
|
||||
"insert_your_address": "Enter your e-mail address",
|
||||
"feed_description": "To follow updates from a computer or smartphone without the need to periodically open this site, use RSS feeds. </p>\n\n<p> With RSS feeds you use a special app to receive updates from sites that interest you. It's a good way to follow many sites quickly, without the need to create an account or other complications. </p>\n\n<li> If you have Android, we recommend <a href=\"https://f-droid.org/en/packages/net.frju.flym/\">Flym</a> or Feeder </li>\n<li> For iPhone / iPad you can use <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\"> Feed4U </a> </li>\n<li> For desktop / laptop we recommend Feedbro, to be installed on <a href=\"https://addons.mozilla.org/en-GB/firefox/addon/feedbroreader/\"> Firefox </a> or <a href=\"https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa\"> Chrome </a>. </li>\n<br/>\nAdding this link to your RSS feed reader will keep you up to date.",
|
||||
"ical_description": "Computers and smartphones are commonly equipped with a calendar app capable of importing a remote calendar.",
|
||||
"list_description": "If you have a website and want to show a list of events, use the following code"
|
||||
},
|
||||
"register": {
|
||||
"description": "Social movements should organize and self-finance.<br/>\n<br/>Before you can publish, <strong> the account must be approved</strong>, consider that <strong> behind this site you will find real people</strong>, so write two lines to let us know what events you would like to publish.",
|
||||
"error": "Error: ",
|
||||
"complete": "Registration has to be confirmed.",
|
||||
"first_user": "Administrator created"
|
||||
},
|
||||
"event": {
|
||||
"anon": "Anon",
|
||||
"anon_description": "You can add an event without registering or logging in, but will have to wait for someone to read it,\nconfirming that it is a suitable event. It will not be possible to modify it.<br/><br/>\nYou can instead <a href='/login'>log in</a> or <a href='/register'>register</a>. Otherwise go ahead and get an answer as soon as possible. ",
|
||||
"same_day": "on same day",
|
||||
"what_description": "Title",
|
||||
"description_description": "Description",
|
||||
"tag_description": "Tag",
|
||||
"media_description": "You can add a flyer (optional)",
|
||||
"added": "Event added",
|
||||
"saved": "Event saved",
|
||||
"added_anon": "Event added, but has yet to be confirmed.",
|
||||
"updated": "Event updated",
|
||||
"where_description": "Where's the event? If not present you can create it.",
|
||||
"confirmed": "Event confirmed",
|
||||
"not_found": "Could not find event",
|
||||
"remove_confirmation": "Are you sure you want to remove this event?",
|
||||
"recurrent": "Recurring",
|
||||
"edit_recurrent": "Edit recurring event:",
|
||||
"show_recurrent": "recurring events",
|
||||
"show_past": "also prior events",
|
||||
"only_future": "only upcoming events",
|
||||
"recurrent_description": "Choose frequency and select days",
|
||||
"multidate_description": "Is it a festival? Choose when it starts and ends",
|
||||
"multidate": "More days",
|
||||
"normal": "Normal",
|
||||
"normal_description": "Choose the day.",
|
||||
"recurrent_1w_days": "Each {days}",
|
||||
"recurrent_2w_days": "A {days} every other",
|
||||
"recurrent_1m_days": "|The {days} of each month|{days} of each month",
|
||||
"recurrent_2m_days": "|The {days} a month every other|The {days} a month every other",
|
||||
"recurrent_1m_ordinal": "The {n} {days} of each month",
|
||||
"recurrent_2m_ordinal": "|The {n} {days} a month every other|The {n} {days} a month every other",
|
||||
"each_week": "Each week",
|
||||
"each_2w": "Every other weeks",
|
||||
"each_month": "Each month",
|
||||
"due": "until",
|
||||
"from": "From",
|
||||
"image_too_big": "The image can't be bigger than 4MB",
|
||||
"interact_with_me_at": "Interact with me on fediverse at",
|
||||
"follow_me_description": "One of the ways to stay up to date on events published here on {title},\nis following the account <u>{account}</u> from the fediverse, for example via Mastodon, and possibly add resources to an event from there.<br/><br/>\nIf you have never heard of Mastodon and the fediverse we recommend reading <a href='https://www.savjee.be/videos/simply-explained/mastodon-and-fediverse-explained/'>this article</a>.<br/><br/>Enter your instance below (e.g. mastodon.social)",
|
||||
"interact_with_me": "Follow me",
|
||||
"remove_recurrent_confirmation": "Are you sure you want to remove this recurring event?\nPast events will be maintained, but no further events will be created.",
|
||||
"import_URL": "Import from URL",
|
||||
"import_ICS": "Import from ICS",
|
||||
"ics": "ICS",
|
||||
"import_description": "You can import events from other platforms and other instances through standard formats (ics and h-event)",
|
||||
"alt_text_description": "Description for people with visual impairments",
|
||||
"choose_focal_point": "Choose the focal point",
|
||||
"remove_media_confirmation": "Do you confirm the image removal?",
|
||||
"download_flyer": "Download flyer"
|
||||
},
|
||||
"admin": {
|
||||
"place_description": "If you have gotten the place or address wrong, you can change it.<br/>All current and past events associated with this place will change address.",
|
||||
"event_confirm_description": "You can confirm events entered by anonymous users here",
|
||||
"delete_user": "Remove",
|
||||
"remove_admin": "Remove admin",
|
||||
"disable_user_confirm": "Are you sure you want to disable {user}?",
|
||||
"delete_user_confirm": "Are you sure you want to remove {user}?",
|
||||
"disable_admin_user_confirm": "Are you sure to remove admin permissions from {user}?",
|
||||
"enable_admin_user_confirm": "Are sure to add admin permissions to {user}",
|
||||
"user_remove_ok": "User removed",
|
||||
"user_create_ok": "User created",
|
||||
"event_remove_ok": "Event removed",
|
||||
"allow_registration_description": "Allow open registrations?",
|
||||
"allow_anon_event": "Allow anonymous events (has to be confirmed)?",
|
||||
"allow_recurrent_event": "Allow recurring events",
|
||||
"recurrent_event_visible": "Show recurring events by default",
|
||||
"federation": "Federation / ActivityPub",
|
||||
"enable_federation": "Turn on federation",
|
||||
"enable_federation_help": "It will be possible to follow this instance from the fediverse",
|
||||
"add_instance": "Add instance",
|
||||
"select_instance_timezone": "Time zone",
|
||||
"enable_resources": "Turn on resources",
|
||||
"enable_resources_help": "Allows adding resources to the event from the fediverse",
|
||||
"hide_boost_bookmark": "Hides boost/bookmarks",
|
||||
"hide_boost_bookmark_help": "Hides the small icons showing the number of boosts and bookmarks coming from the fediverse",
|
||||
"block": "Block",
|
||||
"unblock": "Unblock",
|
||||
"user_add_help": "An e-mail with instructions on confirming the subscription and choosing a password will be sent to the new user",
|
||||
"instance_name": "Instance name",
|
||||
"show_resource": "Show resource",
|
||||
"hide_resource": "Hide resource",
|
||||
"delete_resource": "Delete resource",
|
||||
"delete_resource_confirm": "Are you sure you want to delete this resource?",
|
||||
"block_user": "Block user",
|
||||
"filter_instances": "Filter instances",
|
||||
"filter_users": "Filter users",
|
||||
"resources": "Resources",
|
||||
"user_blocked": "User {user} blocked",
|
||||
"favicon": "Logo",
|
||||
"user_block_confirm": "Are you sure you want to block user {user}?",
|
||||
"instance_block_confirm": "Are you sure you want block instance {instance}?",
|
||||
"delete_announcement_confirm": "Are you sure you want to remove the announcement?",
|
||||
"announcement_remove_ok": "Announce removed",
|
||||
"announcement_description": "In this section you can insert announcements to remain on the homepage",
|
||||
"instance_locale": "Default language",
|
||||
"instance_timezone_description": "Gancio is designed to collect the events of a specific place, such as a city. All events in this place will be shown in the time zone chosen for it.",
|
||||
"instance_locale_description": "Preferred user language for pages. Sometimes messages must be shown in the same language for everyone (for example when publishing via ActivityPub or when sending some e-mails). In these cases the language selected above will be used.",
|
||||
"title_description": "It is used in the title of the page, in the subject of the e-mail to export RSS and ICS feeds.",
|
||||
"description_description": "Appears in the header next to the title",
|
||||
"instance_place": "Indicative place of this instance",
|
||||
"instance_name_help": "ActivityPub's account to follow",
|
||||
"enable_trusted_instances": "Turn on friendly instances",
|
||||
"trusted_instances_help": "The list of friendly instances will be shown in the header",
|
||||
"add_trusted_instance": "Add a friendly instance",
|
||||
"instance_place_help": "The label to show in instances of others",
|
||||
"delete_trusted_instance_confirm": "Do you really want to delete this item from the friend instance menu?",
|
||||
"is_dark": "Dark theme",
|
||||
"add_link": "Add link",
|
||||
"footer_links": "Footer links",
|
||||
"delete_footer_link_confirm": "Sure to remove this link?",
|
||||
"edit_place": "Edit place",
|
||||
"new_announcement": "New announcement",
|
||||
"show_smtp_setup": "Email settings",
|
||||
"smtp_hostname": "SMTP Hostname",
|
||||
"smtp_port": "SMTP Port",
|
||||
"smtp_secure": "SMTP Secure (TLS or STARTTLS)",
|
||||
"smtp_description": "<ul><li>Admin should receive an email when anon event is added (if enabled).</li><li>Admin should receive email of registration request (if enabled).</li><li>User should receive an email of registration request.</li><li>User should receive email of confirmed registration.</li><li>User should receive a confirmation email when subscribed directly by admin.</li><li>Users should receive email to restore password when they forgot it</li></ul>",
|
||||
"smtp_test_success": "A test email is sent to {admin_email}, please check your inbox",
|
||||
"smtp_test_button": "Send a test email",
|
||||
"smtp_use_sendmail": "Use sendmail",
|
||||
"sender_email": "Sender e-mail",
|
||||
"widget": "Widget",
|
||||
"wrong_domain_warning": "The baseurl configured in config.json <b>({baseurl})</b> differs from the one you're visiting <b>({url})</b>",
|
||||
"new_collection": "New collection",
|
||||
"collections_description": "Collections are groupings of events by tags and places. They will be displayed on the home page",
|
||||
"edit_collection": "Edit Collection"
|
||||
},
|
||||
"auth": {
|
||||
"not_confirmed": "Not confirmed yet…",
|
||||
"fail": "Could not log in. Are you sure the password is correct?"
|
||||
},
|
||||
"settings": {
|
||||
"update_confirm": "Do you want to save your modification?",
|
||||
"change_password": "Change your password",
|
||||
"password_updated": "Password changed.",
|
||||
"danger_section": "Dangerous section",
|
||||
"remove_account": "By pressing the following button your user account will be deleted. Events you published won't be.",
|
||||
"remove_account_confirm": "You are about to permanently delete your account"
|
||||
},
|
||||
"error": {
|
||||
"nick_taken": "This nickname is already in use.",
|
||||
"email_taken": "This e-mail is already in use."
|
||||
},
|
||||
"confirm": {
|
||||
"title": "User confirmation",
|
||||
"not_valid": "Something went wrong.",
|
||||
"valid": "Your account is confirmed, you can now <a href=\"/login\">log in</a>"
|
||||
},
|
||||
"ordinal": {
|
||||
"1": "first",
|
||||
"2": "second",
|
||||
"3": "third",
|
||||
"4": "fourth",
|
||||
"5": "fifth",
|
||||
"-1": "last"
|
||||
},
|
||||
"validators": {
|
||||
"required": "{fieldName} is required",
|
||||
"email": "Insert a valid email"
|
||||
},
|
||||
"about": "\n <p><a href='https://gancio.org'>Gancio</a> is a shared agenda for local communities.</p>\n ",
|
||||
"oauth": {
|
||||
"authorization_request": "The application <code>{app}</code> asks for the following authorization on <code>{instance_name}</code>:",
|
||||
"redirected_to": "After confirmation you will be redirected to <code>{url}</code>",
|
||||
"scopes": {
|
||||
"event:write": "Add and edit your events"
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
"completed": "Setup completed",
|
||||
"completed_description": "<p>You can now login with the following user:<br/><br/>User: <b>{email}</b><br/>Password: <b>{password}<b/></p>",
|
||||
"copy_password_dialog": "Yes, you have to copy the password!",
|
||||
"start": "Start",
|
||||
"https_warning": "You're visiting from HTTP, remember to change baseurl in config.json if you switch to HTTPS!"
|
||||
"common": {
|
||||
"add_event": "Add event",
|
||||
"next": "Next",
|
||||
"export": "Export",
|
||||
"send": "Send",
|
||||
"where": "Where",
|
||||
"address": "Address",
|
||||
"when": "When",
|
||||
"what": "What",
|
||||
"media": "Media",
|
||||
"login": "Login",
|
||||
"email": "E-mail",
|
||||
"password": "Password",
|
||||
"register": "Register",
|
||||
"description": "Description",
|
||||
"remove": "Remove",
|
||||
"hide": "Hide",
|
||||
"search": "Search",
|
||||
"edit": "Edit",
|
||||
"info": "Info",
|
||||
"confirm": "Confirm",
|
||||
"admin": "Admin",
|
||||
"users": "Users",
|
||||
"events": "Events",
|
||||
"places": "Places",
|
||||
"settings": "Options",
|
||||
"actions": "Actions",
|
||||
"deactivate": "Turn off",
|
||||
"remove_admin": "Remove admin",
|
||||
"activate": "Activate",
|
||||
"save": "Save",
|
||||
"preview": "Preview",
|
||||
"logout": "Log out",
|
||||
"share": "Share",
|
||||
"name": "Name",
|
||||
"associate": "Associate",
|
||||
"edit_event": "Edit event",
|
||||
"related": "Related",
|
||||
"add": "Add",
|
||||
"logout_ok": "Logged out",
|
||||
"copy": "Copy",
|
||||
"recover_password": "Recover password",
|
||||
"new_password": "New password",
|
||||
"new_user": "New user",
|
||||
"ok": "Ok",
|
||||
"cancel": "Cancel",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable",
|
||||
"me": "You",
|
||||
"password_updated": "Password changed.",
|
||||
"resources": "Resources",
|
||||
"n_resources": "no resource|a resource|{n} resources",
|
||||
"activate_user": "Confirmed",
|
||||
"displayname": "Display name",
|
||||
"federation": "Federation",
|
||||
"set_password": "Set password",
|
||||
"copy_link": "Copy link",
|
||||
"send_via_mail": "Send e-mail",
|
||||
"add_to_calendar": "Add to calendar",
|
||||
"instances": "Instances",
|
||||
"copied": "Copied",
|
||||
"embed": "Embed",
|
||||
"embed_title": "Embed this event on your website",
|
||||
"embed_help": "Copying the following code into your website and the event will be shown like here",
|
||||
"feed": "RSS Feed",
|
||||
"feed_url_copied": "Open the copied feed URL in your RSS feed reader",
|
||||
"follow_me_title": "Follow updates from fediverse",
|
||||
"follow": "Follow",
|
||||
"moderation": "Moderation",
|
||||
"user": "User",
|
||||
"authorize": "Authorize",
|
||||
"title": "Title",
|
||||
"filter": "Filter",
|
||||
"event": "Event",
|
||||
"pause": "Pause",
|
||||
"start": "Start",
|
||||
"fediverse": "Fediverse",
|
||||
"skip": "Skip",
|
||||
"delete": "Remove",
|
||||
"announcements": "Announcements",
|
||||
"url": "URL",
|
||||
"place": "Place",
|
||||
"tags": "Tags",
|
||||
"tag": "Tag",
|
||||
"theme": "Theme",
|
||||
"reset": "Reset",
|
||||
"import": "Import",
|
||||
"max_events": "N. max events",
|
||||
"label": "Label",
|
||||
"collections": "Collections",
|
||||
"close": "Close",
|
||||
"plugins": "Plugins",
|
||||
"help_translate": "Help Translate",
|
||||
"show_map": "Show map",
|
||||
"calendar": "Calendar",
|
||||
"home": "Home",
|
||||
"about": "About",
|
||||
"content": "Content",
|
||||
"admin_actions": "Admin actions",
|
||||
"recurring_event_actions": "Recurring event actions"
|
||||
},
|
||||
"login": {
|
||||
"description": "By logging in you can publish new events.",
|
||||
"check_email": "Check your e-mail inbox and spam.",
|
||||
"not_registered": "Not registered?",
|
||||
"forgot_password": "Forgot your password?",
|
||||
"error": "Could not log in. Check your login info.",
|
||||
"insert_email": "Enter your e-mail address",
|
||||
"ok": "Logged in"
|
||||
},
|
||||
"recover": {
|
||||
"not_valid_code": "Something went wrong."
|
||||
},
|
||||
"export": {
|
||||
"intro": "Unlike the unsocial platforms that do everything to keep users and data about them, we believe that information, like people, must be free. For this you can stay updated on the events you want, without necessarily going through this site.",
|
||||
"email_description": "You can get events that interest sent via e-mail.",
|
||||
"insert_your_address": "Enter your e-mail address",
|
||||
"feed_description": "To follow updates from a computer or smartphone without the need to periodically open this site, use RSS feeds. </p>\n\n<p> With RSS feeds you use a special app to receive updates from sites that interest you. It's a good way to follow many sites quickly, without the need to create an account or other complications. </p>\n\n<li> If you have Android, we recommend <a href=\"https://f-droid.org/en/packages/net.frju.flym/\">Flym</a> or Feeder </li>\n<li> For iPhone / iPad you can use <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\"> Feed4U </a> </li>\n<li> For desktop / laptop we recommend Feedbro, to be installed on <a href=\"https://addons.mozilla.org/en-GB/firefox/addon/feedbroreader/\"> Firefox </a> or <a href=\"https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa\"> Chrome </a>. </li>\n<br/>\nAdding this link to your RSS feed reader will keep you up to date.",
|
||||
"ical_description": "Computers and smartphones are commonly equipped with a calendar app capable of importing a remote calendar.",
|
||||
"list_description": "If you have a website and want to show a list of events, use the following code"
|
||||
},
|
||||
"register": {
|
||||
"description": "Social movements should organize and self-finance.<br/>\n<br/>Before you can publish, <strong> the account must be approved</strong>, consider that <strong> behind this site you will find real people</strong>, so write two lines to let us know what events you would like to publish.",
|
||||
"error": "Error: ",
|
||||
"complete": "Registration has to be confirmed.",
|
||||
"first_user": "Administrator created"
|
||||
},
|
||||
"event": {
|
||||
"anon": "Anon",
|
||||
"anon_description": "You can add an event without registering or logging in, but will have to wait for someone to read it,\nconfirming that it is a suitable event. It will not be possible to modify it.<br/><br/>\nYou can instead <a href='/login'>log in</a> or <a href='/register'>register</a>. Otherwise go ahead and get an answer as soon as possible. ",
|
||||
"same_day": "on same day",
|
||||
"what_description": "Title",
|
||||
"description_description": "Description",
|
||||
"tag_description": "Tag",
|
||||
"media_description": "You can add a flyer (optional)",
|
||||
"added": "Event added",
|
||||
"saved": "Event saved",
|
||||
"added_anon": "Event added, but has yet to be confirmed.",
|
||||
"updated": "Event updated",
|
||||
"where_description": "Where's the event? If not present you can create it.",
|
||||
"address_description": "What is the address?",
|
||||
"address_description_osm": "What is the address? (<a href='http://osm.org/copyright'>OpenStreetMap</a> contributors)",
|
||||
"confirmed": "Event confirmed",
|
||||
"not_found": "Could not find event",
|
||||
"remove_confirmation": "Are you sure you want to remove this event?",
|
||||
"recurrent": "Recurring",
|
||||
"edit_recurrent": "Edit recurring event:",
|
||||
"show_recurrent": "recurring events",
|
||||
"show_past": "also prior events",
|
||||
"only_future": "only upcoming events",
|
||||
"recurrent_description": "Choose frequency and select days",
|
||||
"multidate_description": "Is it a festival? Choose when it starts and ends",
|
||||
"multidate": "More days",
|
||||
"normal": "Normal",
|
||||
"normal_description": "Choose the day.",
|
||||
"recurrent_1w_days": "Each {days}",
|
||||
"recurrent_2w_days": "A {days} every other",
|
||||
"recurrent_1m_days": "The {days} of each month",
|
||||
"recurrent_2m_days": "The {days} a month every other",
|
||||
"recurrent_1m_ordinal": "The {n} {days} of each month",
|
||||
"recurrent_2m_ordinal": "The {n} {days} a month every other",
|
||||
"each_week": "Each week",
|
||||
"each_2w": "Every other weeks",
|
||||
"each_month": "Each month",
|
||||
"due": "until",
|
||||
"from": "From",
|
||||
"image_too_big": "The image can't be bigger than 4MB",
|
||||
"interact_with_me_at": "Interact with me on fediverse at",
|
||||
"follow_me_description": "One of the ways to stay up to date on events published here on {title},\nis following the account <u>{account}</u> from the fediverse, for example via Mastodon, and possibly add resources to an event from there.<br/><br/>\nIf you have never heard of Mastodon and the fediverse we recommend reading <a href='https://www.savjee.be/videos/simply-explained/mastodon-and-fediverse-explained/'>this article</a>.<br/><br/>Enter your instance below (e.g. mastodon.social)",
|
||||
"interact_with_me": "Follow me",
|
||||
"remove_recurrent_confirmation": "Are you sure you want to remove this recurring event?\nPast events will be maintained, but no further events will be created.",
|
||||
"import_URL": "Import from URL",
|
||||
"import_ICS": "Import from ICS",
|
||||
"ics": "ICS",
|
||||
"import_description": "You can import events from other platforms and other instances through standard formats (ics and h-event)",
|
||||
"alt_text_description": "Description for people with visual impairments",
|
||||
"choose_focal_point": "Choose the focal point",
|
||||
"remove_media_confirmation": "Do you confirm the image removal?",
|
||||
"download_flyer": "Download flyer"
|
||||
},
|
||||
"admin": {
|
||||
"place_description": "If you have gotten the place or address wrong, you can change it.<br/>All current and past events associated with this place will change address.",
|
||||
"event_confirm_description": "You can confirm events entered by anonymous users here",
|
||||
"delete_user": "Remove",
|
||||
"remove_admin": "Remove admin",
|
||||
"disable_user_confirm": "Are you sure you want to disable {user}?",
|
||||
"delete_user_confirm": "Are you sure you want to remove {user}?",
|
||||
"disable_admin_user_confirm": "Are you sure to remove admin permissions from {user}?",
|
||||
"enable_admin_user_confirm": "Are you sure to add admin permissions to {user}?",
|
||||
"user_remove_ok": "User removed",
|
||||
"user_create_ok": "User created",
|
||||
"event_remove_ok": "Event removed",
|
||||
"allow_registration_description": "Allow open registrations?",
|
||||
"allow_anon_event": "Allow anonymous events (has to be confirmed)?",
|
||||
"allow_multidate_event": "Allow multi-day events",
|
||||
"allow_recurrent_event": "Allow recurring events",
|
||||
"allow_geolocation": "Allow events geolocation",
|
||||
"recurrent_event_visible": "Show recurring events by default",
|
||||
"federation": "Federation / ActivityPub",
|
||||
"enable_federation": "Turn on federation",
|
||||
"enable_federation_help": "It will be possible to follow this instance from the fediverse",
|
||||
"add_instance": "Add instance",
|
||||
"select_instance_timezone": "Time zone",
|
||||
"enable_resources": "Turn on resources",
|
||||
"enable_resources_help": "Allows adding resources to the event from the fediverse",
|
||||
"hide_boost_bookmark": "Hides boost/bookmarks",
|
||||
"hide_boost_bookmark_help": "Hides the small icons showing the number of boosts and bookmarks coming from the fediverse",
|
||||
"block": "Block",
|
||||
"unblock": "Unblock",
|
||||
"user_add_help": "An e-mail with instructions on confirming the subscription and choosing a password will be sent to the new user",
|
||||
"instance_name": "Instance name",
|
||||
"show_resource": "Show resource",
|
||||
"hide_resource": "Hide resource",
|
||||
"delete_resource": "Delete resource",
|
||||
"delete_resource_confirm": "Are you sure you want to delete this resource?",
|
||||
"delete_tag_confirm": "Are you sure you want to remove the tag \"{tag}\"? The tag will be removed from {n} events.",
|
||||
"block_user": "Block user",
|
||||
"filter_instances": "Filter instances",
|
||||
"filter_users": "Filter users",
|
||||
"resources": "Resources",
|
||||
"user_blocked": "User {user} blocked",
|
||||
"favicon": "Logo",
|
||||
"user_block_confirm": "Are you sure you want to block user {user}?",
|
||||
"instance_block_confirm": "Are you sure you want block instance {instance}?",
|
||||
"delete_announcement_confirm": "Are you sure you want to remove the announcement?",
|
||||
"announcement_remove_ok": "Announce removed",
|
||||
"announcement_description": "In this section you can insert announcements to remain on the homepage",
|
||||
"instance_locale": "Default language",
|
||||
"instance_timezone_description": "Gancio is designed to collect the events of a specific place, such as a city. All events in this place will be shown in the time zone chosen for it.",
|
||||
"instance_locale_description": "Preferred user language for pages. Sometimes messages must be shown in the same language for everyone (for example when publishing via ActivityPub or when sending some e-mails). In these cases the language selected above will be used.",
|
||||
"title_description": "It is used in the title of the page, in the subject of the e-mail to export RSS and ICS feeds.",
|
||||
"description_description": "Appears in the header next to the title",
|
||||
"instance_place": "Indicative place of this instance",
|
||||
"instance_name_help": "ActivityPub's account to follow",
|
||||
"enable_trusted_instances": "Turn on friendly instances",
|
||||
"trusted_instances_help": "The list of friendly instances will be shown in the header",
|
||||
"trusted_instances_label": "Navigation label to friendly instances",
|
||||
"trusted_instances_label_default": "Friendly instances",
|
||||
"trusted_instances_label_help": "The default label is 'Friendly instances'",
|
||||
"add_trusted_instance": "Add a friendly instance",
|
||||
"instance_place_help": "The label to show in instances of others",
|
||||
"delete_trusted_instance_confirm": "Do you really want to delete this item from the friend instance menu?",
|
||||
"is_dark": "Dark theme",
|
||||
"add_link": "Add link",
|
||||
"footer_links": "Footer links",
|
||||
"delete_footer_link_confirm": "Sure to remove this link?",
|
||||
"edit_place": "Edit place",
|
||||
"edit_tag": "Edit tag",
|
||||
"edit_tag_help": "You can change the tag by replacing it with a new one or merging it with an existing one. The {n} associated events will also be changed.",
|
||||
"new_announcement": "New announcement",
|
||||
"show_smtp_setup": "Email settings",
|
||||
"smtp_hostname": "SMTP Hostname",
|
||||
"smtp_port": "SMTP Port",
|
||||
"smtp_secure": "SMTP Secure (TLS or STARTTLS)",
|
||||
"smtp_description": "<ul><li>Admin should receive an email when anon event is added (if enabled).</li><li>Admin should receive email of registration request (if enabled).</li><li>User should receive an email of registration request.</li><li>User should receive email of confirmed registration.</li><li>User should receive a confirmation email when subscribed directly by admin.</li><li>Users should receive email to restore password when they forgot it</li></ul>",
|
||||
"smtp_test_success": "A test email is sent to {admin_email}, please check your inbox",
|
||||
"smtp_test_button": "Send a test email",
|
||||
"smtp_use_sendmail": "Use sendmail",
|
||||
"admin_email": "Admin e-mail",
|
||||
"admin_email_help": "The address we use as the sender to send emails. This is also the address to which admin emails are sent",
|
||||
"widget": "Widget",
|
||||
"wrong_domain_warning": "The baseurl configured in config.json <b>({baseurl})</b> differs from the one you're visiting <b>({url})</b>",
|
||||
"new_collection": "New collection",
|
||||
"collections_description": "Collections are groupings of events by tags and places. They will be displayed on the home page",
|
||||
"edit_collection": "Edit Collection",
|
||||
"config_plugin": "Plugin configuration",
|
||||
"plugins_description": "",
|
||||
"fallback_image": "Fallback image",
|
||||
"header_image": "Header image",
|
||||
"hide_thumbs": "Hide thumbs",
|
||||
"hide_calendar": "Hide calendar",
|
||||
"default_images": "Default images",
|
||||
"default_images_help": "You have to <a href='/admin?tab=theme'>refresh</a> the page to see the changes.",
|
||||
"blocked": "Blocked",
|
||||
"domain": "Domain",
|
||||
"known_users": "Known users",
|
||||
"created_at": "Created at",
|
||||
"geolocation_description": "<b>1. Define a provider for geocoding service</b>.<br>Currently, among those listed in the <a href=\"https://wiki.openstreetmap.org/wiki/Nominatim#Alternatives_.2F_Third-party_providers\">wiki of OpenStreetMap</a>, there is support for software <a href=\"https://github.com/osm-search/Nominatim\">Nominatim</a> and <a href=\"https://github.com/komoot/photon\">Photon</a>.<br>You can use one of the related official demos by copying the link in the 'Geocoding provider' field:<ul><li>https://nominatim.openstreetmap.org/search (<a href=\"https://operations.osmfoundation.org/policies/nominatim/\">Terms of Service</a>)</li><li>https://photon.komoot.io/api/ (<a href=\"https://photon.komoot.io/\">Terms of Service</a>)</li></ul><br><b>2. Define a provider for map layers.</b><br>You can find a list of them here: <a href=\"https://leaflet-extras.github.io/leaflet-providers/preview/\">https://leaflet-extras.github.io/leaflet-providers/preview/</a>",
|
||||
"geocoding_provider_type": "Geocoding software",
|
||||
"geocoding_provider_type_help": "The default software is Nominatim",
|
||||
"geocoding_provider": "Geocoding provider",
|
||||
"geocoding_provider_help": "The default provider is Nominatim",
|
||||
"geocoding_countrycodes": "Country codes",
|
||||
"geocoding_countrycodes_help": "Allows you to set a filter to searches based on area codes",
|
||||
"geocoding_test_button": "Test geocoding",
|
||||
"geocoding_test_success": "The geocoding service at {service_name} is working",
|
||||
"geocoding_test_error": "The geocoding service is not reachable at {service_name}",
|
||||
"tilelayer_provider": "Tilelayer provider",
|
||||
"tilelayer_provider_help": "The default provider is OpenStreetMap",
|
||||
"tilelayer_provider_attribution": "Attribution",
|
||||
"tilelayer_test_button": "Test tilelayer",
|
||||
"tilelayer_test_success": "The tilelayer service at {service_name} is working",
|
||||
"tilelayer_test_error": "The tilelayer service is not reachable at {service_name}",
|
||||
"geolocation": "Geolocation"
|
||||
},
|
||||
"auth": {
|
||||
"not_confirmed": "Not confirmed yet…",
|
||||
"fail": "Could not log in. Are you sure the password is correct?"
|
||||
},
|
||||
"settings": {
|
||||
"update_confirm": "Do you want to save your modification?",
|
||||
"change_password": "Change your password",
|
||||
"password_updated": "Password changed.",
|
||||
"danger_section": "Dangerous section",
|
||||
"remove_account": "By pressing the following button your user account will be deleted. Events you published won't be.",
|
||||
"remove_account_confirm": "You are about to permanently delete your account"
|
||||
},
|
||||
"error": {
|
||||
"nick_taken": "This nickname is already in use.",
|
||||
"email_taken": "This e-mail is already in use."
|
||||
},
|
||||
"confirm": {
|
||||
"title": "User confirmation",
|
||||
"not_valid": "Something went wrong.",
|
||||
"valid": "Your account is confirmed, you can now <a href=\"/login\">log in</a>"
|
||||
},
|
||||
"ordinal": {
|
||||
"1": "first",
|
||||
"2": "second",
|
||||
"3": "third",
|
||||
"4": "fourth",
|
||||
"5": "fifth",
|
||||
"-1": "last"
|
||||
},
|
||||
"validators": {
|
||||
"required": "{fieldName} is required",
|
||||
"email": "Insert a valid email"
|
||||
},
|
||||
"about": "\n <p><a href='https://gancio.org'>Gancio</a> is a shared agenda for local communities.</p>\n ",
|
||||
"oauth": {
|
||||
"authorization_request": "The application <code>{app}</code> asks for the following authorization on <code>{instance_name}</code>:",
|
||||
"redirected_to": "After confirmation you will be redirected to <code>{url}</code>",
|
||||
"scopes": {
|
||||
"event:write": "Add and edit your events"
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
"completed": "Setup completed",
|
||||
"completed_description": "<p>You can now login with the following user:<br/><br/>User: <b>{email}</b><br/>Password: <b>{password}<b/></p>",
|
||||
"copy_password_dialog": "Yes, you have to copy the password!",
|
||||
"start": "Start",
|
||||
"https_warning": "You're visiting from HTTP, remember to change baseurl in config.json if you switch to HTTPS!"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,7 +87,21 @@
|
|||
"reset": "Reset",
|
||||
"theme": "Tema",
|
||||
"label": "Etiqueta",
|
||||
"max_events": "Número de eventos máximo"
|
||||
"max_events": "Número de eventos máximo",
|
||||
"show_map": "Mostrar mapa",
|
||||
"latitude": "Latitud",
|
||||
"longitude": "Longitud",
|
||||
"getting_there": "Cómo llegar",
|
||||
"collections": "Colecciones",
|
||||
"plugins": "Complementos",
|
||||
"home": "Página principal",
|
||||
"about": "Sobre el sitio",
|
||||
"close": "Cerrar",
|
||||
"help_translate": "Ayuda a traducir",
|
||||
"calendar": "Calendario",
|
||||
"content": "Contenido",
|
||||
"admin_actions": "Acciones de administrador",
|
||||
"recurring_event_actions": "Acciones de eventos recurrentes"
|
||||
},
|
||||
"login": {
|
||||
"description": "Entrando podrás publicar nuevos eventos.",
|
||||
|
@ -102,7 +116,7 @@
|
|||
"not_valid_code": "Mmmmm algo no salió bien..."
|
||||
},
|
||||
"export": {
|
||||
"intro": "A diferencia de las plataformas del capitalismo, que hacen todo lo posible para mantener datos y usuarios dentro de ellas, creemos las informaciones, así como las personas, deben ser libres. Para ello, puedes mantenerte enterado sobre los eventos que te interesan como mejor te parezca, sin necesariamente tener que pasar por este sitio.",
|
||||
"intro": "A diferencia de las plataformas antisociales, que hacen todo lo posible para mantener datos y usuarios dentro de ellas, creemos que la información, así como las personas, debe ser libre. Por este motivo, puedes mantenerte enterado sobre los eventos que te interesan como mejor te parezca, sin necesariamente tener que pasar por este sitio.",
|
||||
"email_description": "Puedes recibir por mail los eventos que te interesan.",
|
||||
"insert_your_address": "Casilla de correo",
|
||||
"feed_description": "Para seguir las actualizaciones desde un ordenador o teléfono inteligente sin la necesidad de abrir periódicamente el sitio, el método recomendado es usar los feeds RSS.</p>\n\n<p>Con rss feeds, utilizas una aplicación especial para recibir actualizaciones de los sitios que más te interesan, como por ejemplo éste. Es una buena manera de seguir muchos sitios muy rápidamente, sin la necesidad de crear una cuenta u otras complicaciones.</p>\n\n<li>Si tienes Android, te sugerimos <a href=\"https://f-droid.org/es/packages/com.nononsenseapps.feeder/\">Feeder</a> o Feeder</li>\n<li>Para iPhone/iPad puedes usar <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\">Feed4U</a></li>\n<li>En el caso de un ordenador aconsejamos Feedbro, se instala como plugin <a href=\"https://addons.mozilla.org/es-ES/firefox/addon/feedbroreader/\">de Firefox </a>o <a href=\"https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa\">de Chrome</a> y funciona con todos los principales sistemas.</li>\n<br/>\nAgregando este link a tu lector de feed, estarás siempre actualizado/a.",
|
||||
|
@ -110,7 +124,7 @@
|
|||
"list_description": "Si tienes un sitio web y quieres mostrar una lista de eventos, puedes usar el siguiente código"
|
||||
},
|
||||
"register": {
|
||||
"description": "Los movimientos sociales necesitan organizarse y autofinanciarse. <br/>\nEste es un regalo para ustedes, úsenlo solamente para eventos con fines no comerciales y obviamente antifascistas, antisexistas y antirracistas.\n<br/> Antes de que puedas publicar, <strong> debemos aprobar la cuenta </strong>. Como imaginarás, <strong> detrás de este sitio hay personas de carne y hueso</strong>, por esto te pedimos escribir algo para hacernos saber que tipos de eventos te gustaría publicar.",
|
||||
"description": "Los movimientos sociales necesitan organizarse y autofinanciarse. <br/>\n<br/>Antes de que puedas publicar, <strong> debemos aprobar la cuenta </strong>. Como imaginarás, <strong> detrás de este sitio hay personas de carne y hueso</strong>, por esto te pedimos escribir algo para hacernos saber que tipos de eventos te gustaría publicar.",
|
||||
"error": "Error: ",
|
||||
"complete": "Confirmaremos el registro lo antes posible.",
|
||||
"first_user": "Administrador creado y activado"
|
||||
|
@ -126,6 +140,8 @@
|
|||
"added": "Evento agregado",
|
||||
"added_anon": "Evento agregado, será confirmado cuanto antes.",
|
||||
"where_description": "¿Dónde es? Si el lugar no está, escribilo.",
|
||||
"coordinates_search": "Buscar coordenadas",
|
||||
"coordinates_search_description": "Puede buscar el lugar por nombre o pegar el par de coordenadas.",
|
||||
"confirmed": "Evento confirmado",
|
||||
"not_found": "Evento no encontrado",
|
||||
"remove_confirmation": "¿Estás seguro/a de querér eliminar este evento?",
|
||||
|
@ -137,16 +153,16 @@
|
|||
"normal_description": "Selecciona el día.",
|
||||
"recurrent_1w_days": "Cada {days}",
|
||||
"recurrent_2w_days": "Un {days} cada dos",
|
||||
"recurrent_1m_days": "|El día {days} de cada mes|Los días {days} de cada mes",
|
||||
"recurrent_2m_days": "|El día {days} cada dos meses|Los días {days} cada dos meses",
|
||||
"recurrent_1m_days": "El día {days} de cada mes",
|
||||
"recurrent_2m_days": "El día {days} cada dos meses",
|
||||
"recurrent_1m_ordinal": "El {n} {days} de cada mes",
|
||||
"recurrent_2m_ordinal": "|El {n} {days} un mes sí y el otro no|El {n} {days} un mes sí y el otro no",
|
||||
"recurrent_2m_ordinal": "El {n} {days} un mes sí y el otro no",
|
||||
"each_week": "Cada semana",
|
||||
"each_2w": "Cada dos semanas",
|
||||
"each_month": "Cada mes",
|
||||
"due": "a las",
|
||||
"from": "Desde las",
|
||||
"image_too_big": "La imagén es demasiado grande! Tamaño máx 4M",
|
||||
"image_too_big": "La imagen no puede ser mayor a 4 MB",
|
||||
"interact_with_me_at": "Sígueme en el fediverso en",
|
||||
"show_recurrent": "Eventos recurrentes",
|
||||
"show_past": "eventos pasados",
|
||||
|
@ -160,7 +176,13 @@
|
|||
"import_description": "Puedes importar eventos de otras plataformas y otras instancias mediante formatos estandars (ics y h-event)",
|
||||
"edit_recurrent": "Editar evento recurrente:",
|
||||
"updated": "Evento actualizado",
|
||||
"saved": "Evento guardado"
|
||||
"saved": "Evento guardado",
|
||||
"choose_focal_point": "Elige el punto central",
|
||||
"address_description_osm": "¿Cuál es la dirección? (colaboradores de <a href='http://osm.org/copyright'>OpenStreetMap</a>)",
|
||||
"address_description": "¿Cuál es la dirección?",
|
||||
"alt_text_description": "Descripción para personas con baja visión",
|
||||
"download_flyer": "Descargar folleto",
|
||||
"remove_media_confirmation": "¿Confirmas la eliminación de la imagen?"
|
||||
},
|
||||
"admin": {
|
||||
"place_description": "En el caso de que un lugar sea incorrecto o cambie de dirección, puedes cambiarlo. <br/> Todos los eventos presentes y pasados asociados con este lugar cambiarán de dirección.",
|
||||
|
@ -174,6 +196,7 @@
|
|||
"allow_anon_event": "¿Se pueden ingresar eventos anónimos (sujeto a confirmación)?",
|
||||
"allow_comments": "Habilitar comentarios",
|
||||
"allow_recurrent_event": "Habilitar eventos fijos",
|
||||
"allow_geolocation": "Habilitar la geolocalización de eventos",
|
||||
"recurrent_event_visible": "Eventos fijos visibles por defecto",
|
||||
"federation": "Federación / ActivityPub",
|
||||
"enable_federation": "Habilitar la federación",
|
||||
|
@ -210,6 +233,9 @@
|
|||
"instance_name_help": "Nombre de la cuenta de ActivityPub a seguir",
|
||||
"enable_trusted_instances": "Habilitar las solicitudes amistosas",
|
||||
"trusted_instances_help": "Las instancias amistosas aparecerán en la barra de navegación en la parte superior de la página",
|
||||
"trusted_instances_label": "Etichetta di navigazione alle istanze amiche",
|
||||
"trusted_instances_label_default": "Instancias amigables",
|
||||
"trusted_instances_label_help": "La etiqueta predeterminada es 'Instancias amigables'",
|
||||
"add_trusted_instance": "Añade una instancia de amigo",
|
||||
"instance_place_help": "Esta cadena se mostrará en el menú de las otras instancias amigas",
|
||||
"delete_trusted_instance_confirm": "¿Realmente quieres borrar este elemento del menú de instancias amistosas?",
|
||||
|
@ -228,7 +254,34 @@
|
|||
"smtp_test_success": "Un correo de prueba se ha enviado a {admin_email}. Por favos, comprueba tu bandeja de entrada",
|
||||
"smtp_test_button": "Enviar correo de prueba",
|
||||
"admin_email": "Correo del administrador",
|
||||
"smtp_description": "<ul><li>El administrador debería recibir un correo cuando son añadidos eventos anónimos (si está habilitado).</li><li>El administrador debería recibir un correo de petición de registro (si está habilitado).</li><li>El usuario debería recibir un correo de petición de registro.</li><li>El usuario debería recibir un correo de confirmación de registro.</li><li>El usuario debería recibir un correo de confirmación cuando el administrador le subscriba directamente.</li><li>El usuario debería recibir un correo para restaurar la contraseña cuando la haya olvidado.</li></ul>"
|
||||
"smtp_description": "<ul><li>El administrador debería recibir un correo cuando son añadidos eventos anónimos (si está habilitado).</li><li>El administrador debería recibir un correo de petición de registro (si está habilitado).</li><li>El usuario debería recibir un correo de petición de registro.</li><li>El usuario debería recibir un correo de confirmación de registro.</li><li>El usuario debería recibir un correo de confirmación cuando el administrador le subscriba directamente.</li><li>El usuario debería recibir un correo para restaurar la contraseña cuando la haya olvidado.</li></ul>",
|
||||
"disable_admin_user_confirm": "¿Estás seguro que quieres quitar los permisos de administrador de {user}?",
|
||||
"enable_admin_user_confirm": "¿Estás seguro que quieres conceder permisos de administrador a {user}?",
|
||||
"smtp_port": "Puerto SMTP",
|
||||
"smtp_secure": "SMTP Seguro (TLS o STARTTLS)",
|
||||
"edit_collection": "Editar colección",
|
||||
"config_plugin": "Configuración de complemento",
|
||||
"fallback_image": "Imagen por defecto",
|
||||
"header_image": "Imagan de encabezado",
|
||||
"hide_thumbs": "Esconder miniaturas",
|
||||
"hide_calendar": "Esconder calendario",
|
||||
"default_images": "Imágenes por defecto",
|
||||
"sender_email": "Correo de remitente",
|
||||
"event_remove_ok": "Evento eliminado",
|
||||
"smtp_use_sendmail": "Usar sendmail",
|
||||
"wrong_domain_warning": "El parámetro baseurl configurado en config.json <b>({baseurl})</b> difiere del que estás visitando <b>({url})</b>",
|
||||
"new_collection": "Nueva colección",
|
||||
"collections_description": "Las colecciones son agrupaciones de eventos por etiquetas y ubicaciones. Serán desplegadas en la página de inicio",
|
||||
"domain": "Dominio",
|
||||
"created_at": "Creado a las",
|
||||
"blocked": "Bloqueado",
|
||||
"known_users": "Usuarios conocidos",
|
||||
"default_images_help": "Tienes que <a href='/admin?tab=theme'>actualizas</a> la página para ver los cambios.",
|
||||
"geocoding_provider": "Proveedor de geocodificación",
|
||||
"admin_email_help": "La dirección que usamos como remitente para enviar emails. Esta es también la dirección a la cual los emails de administración son enviados",
|
||||
"allow_multidate_event": "Permitir eventos de múltiples días",
|
||||
"geocoding_countrycodes": "Códigos de país",
|
||||
"geocoding_provider_help": "El proveedor por defecto es Nominatim"
|
||||
},
|
||||
"auth": {
|
||||
"not_confirmed": "Todavía no hemos confirmado este email…",
|
||||
|
@ -274,6 +327,8 @@
|
|||
"setup": {
|
||||
"completed": "Configuración completada",
|
||||
"start": "Inicio",
|
||||
"completed_description": "<p>Puedes ingresar con el siguiente usuario:<br/><br/>Usuario: <b>{email}</b><br/>Contraseña: <b>{password}<b/></p>"
|
||||
"completed_description": "<p>Puedes ingresar con el siguiente usuario:<br/><br/>Usuario: <b>{email}</b><br/>Contraseña: <b>{password}<b/></p>",
|
||||
"copy_password_dialog": "Sí, ¡tienes que copiar la contraseña!",
|
||||
"https_warning": "¡Si estás visitando a través de HTTP, recuerda cambiar el parámetro baseurl en config.json si cambias a HTTPS!"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
export default {
|
||||
ca: 'Català',
|
||||
de: 'Deutsch',
|
||||
en: 'English',
|
||||
es: 'Español',
|
||||
eu: 'Euskara',
|
||||
fr: 'Francais',
|
||||
gl: 'Galego',
|
||||
it: 'Italiano',
|
||||
nb: 'Norwegian Bokmål',
|
||||
pl: 'Polski',
|
||||
sk: 'Slovak'
|
||||
}
|
|
@ -25,7 +25,7 @@
|
|||
"events": "Ekitaldiak",
|
||||
"places": "Lekuak",
|
||||
"settings": "Aukerak",
|
||||
"actions": "Ekintzak",
|
||||
"actions": "Eragiketak",
|
||||
"deactivate": "Desaktibatu",
|
||||
"remove_admin": "Kendu administratzaile baimena",
|
||||
"activate": "Aktibatu",
|
||||
|
@ -89,7 +89,16 @@
|
|||
"tags": "Etiketak",
|
||||
"close": "Itxi",
|
||||
"blobs": "Mordoak",
|
||||
"collections": "Bildumak"
|
||||
"collections": "Bildumak",
|
||||
"plugins": "Pluginak",
|
||||
"help_translate": "Lagundu itzultzen",
|
||||
"show_map": "Erakutsi mapa",
|
||||
"calendar": "Egutegia",
|
||||
"home": "Etxea",
|
||||
"about": "Honi buruz",
|
||||
"recurring_event_actions": "Ekitaldi errepikarien eragiketak",
|
||||
"content": "Edukia",
|
||||
"admin_actions": "Administratzaile eragiketak"
|
||||
},
|
||||
"login": {
|
||||
"description": "Saioa hasten baduzu ekitaldi berriak sortu ahal izango dituzu.",
|
||||
|
@ -142,10 +151,10 @@
|
|||
"normal_description": "Aukeratu eguna.",
|
||||
"recurrent_1w_days": "{days}(e)ro",
|
||||
"recurrent_2w_days": "Bi {days}(e)z behin",
|
||||
"recurrent_1m_days": "|Hilabetero {days}etan|Hilabetero {days}etan",
|
||||
"recurrent_2m_days": "|{days}etan bi hilabetez behin|{days}etan bi hilabetez behin",
|
||||
"recurrent_1m_days": "Hilabetero {days}etan",
|
||||
"recurrent_2m_days": "{days}etan bi hilabetez behin",
|
||||
"recurrent_1m_ordinal": "Hilabeteko {n}. {days}(e)ro",
|
||||
"recurrent_2m_ordinal": "|Bi hilabetez behin {n}. {days}(e)an|Bi hilabetez behin {n}. {days}(e)an",
|
||||
"recurrent_2m_ordinal": "Bi hilabetez behin {n}. {days}(e)an",
|
||||
"each_week": "Astero",
|
||||
"each_2w": "Bi astez behin",
|
||||
"each_month": "Hilero",
|
||||
|
@ -166,7 +175,9 @@
|
|||
"remove_media_confirmation": "Irudiaren ezabaketa baieztatzen duzu?",
|
||||
"alt_text_description": "Ikusmen-urritasunak dituztenentzako deskripzioa",
|
||||
"choose_focal_point": "Aukeratu arretagunea",
|
||||
"download_flyer": "Deskargatu eskuorria"
|
||||
"download_flyer": "Deskargatu eskuorria",
|
||||
"address_description": "Zein da helbidea?",
|
||||
"address_description_osm": "Zein da helbidea? (<a href='http://osm.org/copyright'>OpenStreetMap</a> kolaboratzaileak)"
|
||||
},
|
||||
"admin": {
|
||||
"place_description": "Lekua edo helbidea oker badago, alda dezakezu.<br/>Leku honekin lotutako iraganeko eta etorkizuneko ekitaldien helbidea aldatuko da.",
|
||||
|
@ -215,6 +226,9 @@
|
|||
"instance_name_help": "Jarraitu beharreko ActivityPub kontua",
|
||||
"enable_trusted_instances": "Gaitu instantzia adiskidetsuak",
|
||||
"trusted_instances_help": "Kideak diren instantzien zerrenda goiburuan erakutsiko da",
|
||||
"trusted_instances_label": "Nabigazio etiketa lagunarteko instantzietarako",
|
||||
"trusted_instances_label_default": "Instantzia atseginak",
|
||||
"trusted_instances_label_help": "Etiketa lehenetsia 'Instantzia atseginak' da",
|
||||
"add_trusted_instance": "Gehitu kide den instantzia bat",
|
||||
"instance_place_help": "Beste instantzietan agertuko den izena",
|
||||
"delete_trusted_instance_confirm": "Ziur al zaude instantzia hau menuko zerrendatik kendu nahi duzula?",
|
||||
|
@ -246,8 +260,38 @@
|
|||
"edit_collection": "Editatu bilduma",
|
||||
"disable_admin_user_confirm": "Ziur al zaude {user}(r)i administratzaile baimenak kendu nahi dizkiozula?",
|
||||
"collections_description": "Bildumak, etiketen eta lekuen arabera multzokatutako ekitaldiak dira. Hasierako orrialdean bistaratuko dira",
|
||||
"enable_admin_user_confirm": "Ziur zaude {user}-(r)i administratzaile baimenak gehitu nahi dizkiozula",
|
||||
"new_collection": "Bilduma berria"
|
||||
"enable_admin_user_confirm": "Ziur zaude {user}-(r)i administratzaile baimenak gehitu nahi dizkiozula?",
|
||||
"new_collection": "Bilduma berria",
|
||||
"allow_geolocation": "Baimendu ekitaldiak geokokatzea",
|
||||
"hide_thumbs": "Ezkutatu iruditxoak",
|
||||
"hide_calendar": "Ezkutatu egutegia",
|
||||
"config_plugin": "Pluginaren konfigurazioa",
|
||||
"fallback_image": "Lehenetsitako irudia",
|
||||
"header_image": "Goiburuko irudia",
|
||||
"default_images": "Lehenetsitako irudiak",
|
||||
"blocked": "Blokeatuta",
|
||||
"domain": "Domeinua",
|
||||
"default_images_help": "Orrialdea <a href='/admin?tab=theme'>freskatu</a> behar duzu aldaketak ikusteko.",
|
||||
"known_users": "Erabiltzaile ezagunak",
|
||||
"created_at": "Noiz sortua:",
|
||||
"geocoding_provider_type_help": "Software lehenetsia Nominatim da",
|
||||
"geocoding_provider": "Geokodeketaren hornitzailea",
|
||||
"geocoding_countrycodes": "Estatuen kodeak",
|
||||
"geocoding_countrycodes_help": "Baimendu iragazki bat ezartzen zonalde kodearen arabera",
|
||||
"geocoding_test_button": "Probatu geokodeketa",
|
||||
"tilelayer_provider_help": "Lehenetsitako hornitzailea OpenStreetMap da",
|
||||
"tilelayer_provider_attribution": "Atribuzioa",
|
||||
"geocoding_test_error": "Geokodeketa zerbitzua {service_name}-(e)n ez dago atzigarri",
|
||||
"tilelayer_provider": "Lauza-geruzen hornitzailea",
|
||||
"tilelayer_test_button": "Probatu lauza-geruza",
|
||||
"tilelayer_test_error": "Lauza-geruzen zerbitzua {service_name}(e)n ez dago atzigarri",
|
||||
"admin_email_help": "Epostak bidaltzeko erabiltzen dugun helbidea. Administrazio epostak ere horra bidaltzen dira",
|
||||
"allow_multidate_event": "Baimendu egun anitzeko ekitaldiak",
|
||||
"geocoding_provider_type": "Geokodeketarako softwarea",
|
||||
"geocoding_provider_help": "Lehenetsitako hornitzailea Nominatim da",
|
||||
"geolocation": "Geokokapena",
|
||||
"geocoding_test_success": "Geokodeketa zerbitzua {service_name}-(e)n martxan dago",
|
||||
"tilelayer_test_success": "Lauza-geruzen zerbitzua {service_name}(e)n martxan dago"
|
||||
},
|
||||
"auth": {
|
||||
"not_confirmed": "Oraindik baieztatu gabe dago…",
|
||||
|
|
|
@ -89,7 +89,11 @@
|
|||
"max_events": "Nb. max d'événements",
|
||||
"blobs": "Blobs",
|
||||
"close": "Fermer",
|
||||
"collections": "Collections"
|
||||
"collections": "Collections",
|
||||
"show_map": "Afficher la carte",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"getting_there": "Comment s'y rendre"
|
||||
},
|
||||
"event": {
|
||||
"follow_me_description": "Une des manières de rester informé sur les évènements publiés ici sur {title}\nest de suivre le compte <u>{account}</u> sur le fediverse, par exemple via Mastodon, et pourquoi pas d'ajouter des ressources à un évènement à partir de là.<br/><br/>\nSi vous n'avez jamais entendu parler de Mastodon and du fediverse, nous vous recommandons de lire <a href='https://www.savjee.be/videos/simply-explained/mastodon-and-fediverse-explained/'>cet article (en anglais)</a>.<br/><br/>Saisissez votre nom d'instance ci-dessous (par ex. mastodon.social)",
|
||||
|
@ -104,7 +108,7 @@
|
|||
"from": "Du",
|
||||
"each_month": "Chaque mois",
|
||||
"each_week": "Chaque semaine",
|
||||
"recurrent_1m_days": "|Chaque {days} du mois|Chaque {days} du mois",
|
||||
"recurrent_1m_days": "Chaque {days} du mois",
|
||||
"recurrent_1w_days": "Chaque {days}",
|
||||
"normal_description": "Choisissez un jour.",
|
||||
"normal": "Normal",
|
||||
|
@ -119,6 +123,8 @@
|
|||
"not_found": "Impossible de trouver l'évènement",
|
||||
"confirmed": "Évènement confirmé",
|
||||
"where_description": "Où est l'évènement ? S'il n'apparaît pas, vous pouvez le créer.",
|
||||
"coordinates_search": "Recherche de coordonnées",
|
||||
"coordinates_search_description": "Vous pouvez rechercher le lieu par son nom ou coller la paire de coordonnées",
|
||||
"added_anon": "Évènement ajouté, mais il doit encore être confirmé.",
|
||||
"added": "Évènement ajouté",
|
||||
"media_description": "Vous pouvez ajouter un tract (facultatif)",
|
||||
|
@ -130,8 +136,8 @@
|
|||
"anon": "Anonyme",
|
||||
"ics": "ICS",
|
||||
"each_2w": "Une semaine sur deux",
|
||||
"recurrent_2m_ordinal": "|Le {n} {days} du mois un mois sur|Les {n} {days} du mois un mois sur deux",
|
||||
"recurrent_2m_days": "|Le {days} un mois sur deux|Les {jours} un mois sur deux",
|
||||
"recurrent_2m_ordinal": "Les {n} {days} du mois un mois sur deux",
|
||||
"recurrent_2m_days": "Le {days} un mois sur deux",
|
||||
"recurrent_2w_days": "Un {days} sur deux",
|
||||
"edit_recurrent": "Modifier l’évènement récurrent :",
|
||||
"updated": "Évènement mis à jour",
|
||||
|
@ -168,6 +174,9 @@
|
|||
"instance_place_help": "Le nom à afficher dans les autres instances",
|
||||
"add_trusted_instance": "Ajouter une instance associée",
|
||||
"trusted_instances_help": "La liste des instances associées seront affichées dans l'entête",
|
||||
"trusted_instances_label": "Étiquette de navigation vers les instances amies",
|
||||
"trusted_instances_label_default": "Instances amies",
|
||||
"trusted_instances_label_help": "L'étiquette par défaut est 'Instances amies'",
|
||||
"enable_trusted_instances": "Activer les instances associées",
|
||||
"instance_name_help": "Compte ActivityPub à suivre",
|
||||
"instance_place": "Lieu indicatif de cette instance",
|
||||
|
@ -204,6 +213,7 @@
|
|||
"allow_recurrent_event": "Autoriser les évènements récurrents",
|
||||
"allow_anon_event": "Autoriser les évènements anonymes (doivent être confirmés) ?",
|
||||
"allow_registration_description": "Autoriser l'ouverture des inscriptions ?",
|
||||
"allow_geolocation": "Autoriser la géolocalisation des événements",
|
||||
"user_create_ok": "Utilisateur créé",
|
||||
"user_remove_ok": "Utilisateur supprimé",
|
||||
"delete_user_confirm": "Êtes-vous sûr·e de vouloir supprimer {user} ?",
|
||||
|
|
|
@ -89,7 +89,19 @@
|
|||
"tags": "Cancelos",
|
||||
"close": "Pechar",
|
||||
"blobs": "Blobs",
|
||||
"collections": "Coleccións"
|
||||
"collections": "Coleccións",
|
||||
"show_map": "Mostrar mapa",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"getting_there": "Chegar lá",
|
||||
"plugins": "Complementos",
|
||||
"help_translate": "Axuda coa tradución",
|
||||
"calendar": "Calendario",
|
||||
"home": "Inicio",
|
||||
"about": "Acerca de",
|
||||
"content": "Contido",
|
||||
"admin_actions": "Accións de Admin",
|
||||
"recurring_event_actions": "Accións de eventos recurrentes"
|
||||
},
|
||||
"recover": {
|
||||
"not_valid_code": "Algo fallou."
|
||||
|
@ -114,13 +126,15 @@
|
|||
"saved": "Evento gardado",
|
||||
"updated": "Evento actualizado",
|
||||
"where_description": "Onde será o evento? Se non existe podes crealo.",
|
||||
"coordinates_search": "Procurar coordenadas",
|
||||
"coordinates_search_description": "Pode procurar o lugar pelo nome ou colar o par de coordenadas.",
|
||||
"not_found": "Non atopamos o evento",
|
||||
"show_recurrent": "eventos recurrentes",
|
||||
"show_past": "tamén eventos previos",
|
||||
"only_future": "só eventos futuros",
|
||||
"recurrent_2w_days": "Cada {days} días",
|
||||
"recurrent_2m_ordinal": "|O {n} {days} en meses alternos|O {n} {days} en meses alternos",
|
||||
"recurrent_2m_days": "|O {days} cada dous meses|O {days} cada dous meses",
|
||||
"recurrent_2m_ordinal": "O {n} {days} en meses alternos",
|
||||
"recurrent_2m_days": "O {days} cada dous meses",
|
||||
"each_month": "Cada mes",
|
||||
"due": "ata",
|
||||
"from": "Desde",
|
||||
|
@ -149,7 +163,7 @@
|
|||
"recurrent_1w_days": "Cada {days}",
|
||||
"recurrent_1m_ordinal": "O {n} {days} de cada mes",
|
||||
"image_too_big": "A imaxe non pode superar os 4MB",
|
||||
"recurrent_1m_days": "|O {days} de cada mes|{days} de cada mes",
|
||||
"recurrent_1m_days": "O {days} de cada mes",
|
||||
"each_2w": "Cada dúas semanas",
|
||||
"follow_me_description": "Un dos xeitos de recibir actualizacións dos eventos que se publican aquí en {title},\né seguindo a conta <u>{account}</u> no fediverso, por exemplo a través de Mastodon, e posiblemente tamén engadir recursos para un evento desde alí.<br/><br/>\nSe nunco escoitaches falar de Mastodon e o fediverso recomendámosche ler <a href='https://www.savjee.be/videos/simply-explained/mastodon-and-fediverse-explained/'>este artigo</a>.<br/><br/>Escribe aquí a túa instancia (ex. mastodon.social)",
|
||||
"ics": "ICS",
|
||||
|
@ -157,7 +171,9 @@
|
|||
"alt_text_description": "Descrición para persoas con problemas de visión",
|
||||
"choose_focal_point": "Elixe onde centrar a atención",
|
||||
"remove_media_confirmation": "Confirmas a eliminación da imaxe?",
|
||||
"download_flyer": "Descargar folleto"
|
||||
"download_flyer": "Descargar folleto",
|
||||
"address_description": "Cal é o enderezo?",
|
||||
"address_description_osm": "Cal é o enderezo? (Contribuíntes a <a href='http://osm.org/copyright'>OpenStreetMap</a>)"
|
||||
},
|
||||
"admin": {
|
||||
"place_description": "Se escribiches mal o lugar ou enderezo, podes cambialo.<br/>Cambiará o enderezo de tódolos eventos actuais e pasados asociados a este lugar.",
|
||||
|
@ -187,6 +203,7 @@
|
|||
"resources": "Recursos",
|
||||
"allow_registration_description": "Permitir o rexistro libre?",
|
||||
"allow_anon_event": "Permitir eventos anónimos (haberá que confirmalos)?",
|
||||
"allow_geolocation": "Permitir a geolocalização de eventos",
|
||||
"event_confirm_description": "Aquí podes confirmar os eventos engadidos por usuarias anónimas",
|
||||
"remove_admin": "Eliminar admin",
|
||||
"delete_user": "Eliminar",
|
||||
|
@ -214,6 +231,9 @@
|
|||
"instance_locale_description": "Idioma preferido para as páxinas. A veces as mensaxes teñen que mostrarse no mesmo idioma para tódalas persoas (por exemplo cando publicas vía ActivityPub ou cando envías os emails). Nestos casos usarase o idioma elexido aquí arriba.",
|
||||
"enable_trusted_instances": "Activar instancias amigas",
|
||||
"trusted_instances_help": "A lista das instancias amigas será mostrada na cabeceira",
|
||||
"trusted_instances_label": "Etiqueta de navegação para instâncias de amigos",
|
||||
"trusted_instances_label_default": "Casos amigáveis",
|
||||
"trusted_instances_label_help": "A etiqueta padrão é 'Instâncias amigáveis'",
|
||||
"delete_trusted_instance_confirm": "Tes a certeza de querer eliminar este elemento do menú de instancias amigas?",
|
||||
"instance_place_help": "A etiqueta a mostrar nas instancias de outras",
|
||||
"add_link": "Engadir ligazón",
|
||||
|
@ -237,8 +257,38 @@
|
|||
"disable_admin_user_confirm": "Tes certeza de querer retirar os permisos de administración a {user}?",
|
||||
"sender_email": "Remitente",
|
||||
"collections_description": "As coleccións son agrupamentos de eventos por etiqueta ou lugar. Pode ser mostrado na páxina de inicio",
|
||||
"enable_admin_user_confirm": "Tes a certeza de querer darlle permisos de administración a {user}?",
|
||||
"smtp_use_sendmail": "Usar sendmail"
|
||||
"enable_admin_user_confirm": "Tes a certeza de engadir permiso de admin a {user}?",
|
||||
"smtp_use_sendmail": "Usar sendmail",
|
||||
"config_plugin": "Configuración do complemento",
|
||||
"known_users": "Usuarias coñecidas",
|
||||
"created_at": "Creado o",
|
||||
"fallback_image": "Imaxe por omisión",
|
||||
"header_image": "Imaxe da cabeceira",
|
||||
"hide_thumbs": "Agochar miniaturas",
|
||||
"hide_calendar": "Agochar calendario",
|
||||
"default_images": "Imaxes por defecto",
|
||||
"blocked": "Bloqueado",
|
||||
"domain": "Dominio",
|
||||
"default_images_help": "Tes que <a href='/admin?tab=theme'>actualizar</a> a páxina para ver os cambios.",
|
||||
"geocoding_provider_type": "Software Geocoding",
|
||||
"geocoding_provider_type_help": "O software por defecto é Nominatim",
|
||||
"geocoding_provider": "Provedor Geocoding",
|
||||
"geocoding_provider_help": "O provedor por defecto é Nominatim",
|
||||
"geocoding_countrycodes": "Códigos de país",
|
||||
"geocoding_countrycodes_help": "Permíteche establecer un filtro para as buscas en función do código",
|
||||
"geocoding_test_button": "Comproba a codificación",
|
||||
"geocoding_test_success": "O servizo geocoding en {service_name} funciona",
|
||||
"geocoding_test_error": "O servizo geocoding non está accesible en {service_name}",
|
||||
"tilelayer_provider": "Provedor de teselas do mapa",
|
||||
"tilelayer_provider_help": "O provedor por defecto é OpenStreetMap",
|
||||
"tilelayer_provider_attribution": "Atribución",
|
||||
"tilelayer_test_button": "Comproba as capas",
|
||||
"tilelayer_test_success": "O servizo de capas en {service_name} funciona",
|
||||
"tilelayer_test_error": "O servizo de capas en {service_name} non está accesible",
|
||||
"geolocation": "Xeolocalización",
|
||||
"allow_multidate_event": "Permitir eventos de varios días",
|
||||
"admin_email_help": "O enderezo que se usará como remitente para os emails. Tamén é o enderezo ao que se envían emails de administración",
|
||||
"geolocation_description": "<b>1. Define un provedor para o servizo geocoding</b>.<br>Actualmente, entre os que aparecen na <a href=\"https://wiki.openstreetmap.org/wiki/Nominatim#Alternatives_.2F_Third-party_providers\">wiki de OpenStreetMap</a>, temos soporte para <a href=\"https://github.com/osm-search/Nominatim\">Nominatim</a> e <a href=\"https://github.com/komoot/photon\">Photon</a>.<br>Podes usar unha das demos oficiais indicadas copiando a ligazón no campo 'Provedor Geocoding':<ul><li>https://nominatim.openstreetmap.org/search (<a href=\"https://operations.osmfoundation.org/policies/nominatim/\">Termos do Servizo</a>)</li><li>https://photon.komoot.io/api/ (<a href=\"https://photon.komoot.io/\">Termos do Servizo</a>)</li></ul><br><b>2. Define un provedor para capas do mapa.</b><br>Aquí hai unha lista: <a href=\"https://leaflet-extras.github.io/leaflet-providers/preview/\">https://leaflet-extras.github.io/leaflet-providers/preview/</a>"
|
||||
},
|
||||
"auth": {
|
||||
"not_confirmed": "Aínda non foi confirmado…",
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
module.exports = {
|
||||
en: 'English',
|
||||
eu: 'Euskara',
|
||||
ca: 'Català',
|
||||
de: 'Deutsch',
|
||||
en: 'English',
|
||||
es: 'Español',
|
||||
eu: 'Euskara',
|
||||
fr: 'Francais',
|
||||
gl: 'Galego',
|
||||
it: 'Italiano',
|
||||
ca: 'Català',
|
||||
fr: 'Francais',
|
||||
nb: 'Norwegian Bokmål',
|
||||
pl: 'Polski',
|
||||
pt: 'Português',
|
||||
sk: 'Slovak',
|
||||
pl: 'Polski'
|
||||
zh: '中国'
|
||||
}
|
||||
|
|
617
locales/it.json
617
locales/it.json
|
@ -1,288 +1,335 @@
|
|||
{
|
||||
"common": {
|
||||
"add_event": "Aggiungi evento",
|
||||
"next": "Continua",
|
||||
"export": "Esporta",
|
||||
"send": "Invia",
|
||||
"where": "Dove",
|
||||
"address": "Indirizzo",
|
||||
"when": "Quando",
|
||||
"what": "Cosa",
|
||||
"media": "Media",
|
||||
"login": "Entra",
|
||||
"email": "E-mail",
|
||||
"password": "Password",
|
||||
"register": "Registrati",
|
||||
"description": "Descrizione",
|
||||
"remove": "Elimina",
|
||||
"hide": "Nascondi",
|
||||
"search": "Cerca",
|
||||
"edit": "Modifica",
|
||||
"info": "Info",
|
||||
"confirm": "Conferma",
|
||||
"admin": "Amministra",
|
||||
"users": "Utenti",
|
||||
"events": "Eventi",
|
||||
"places": "Luoghi",
|
||||
"settings": "Opzioni",
|
||||
"actions": "Azioni",
|
||||
"deactivate": "Disattiva",
|
||||
"remove_admin": "Rimuovi Admin",
|
||||
"activate": "Attiva",
|
||||
"save": "Salva",
|
||||
"preview": "Anteprima",
|
||||
"logout": "Esci",
|
||||
"share": "Esporta",
|
||||
"name": "Nome",
|
||||
"associate": "Associa",
|
||||
"edit_event": "Modifica evento",
|
||||
"related": "Memoria storica",
|
||||
"add": "Aggiungi",
|
||||
"logout_ok": "Uscita correttamente",
|
||||
"copy": "Copia",
|
||||
"recover_password": "Recupera password",
|
||||
"new_password": "Nuova password",
|
||||
"new_user": "Nuovo utente",
|
||||
"ok": "OK",
|
||||
"cancel": "Annulla",
|
||||
"enable": "Abilita",
|
||||
"disable": "Disabilita",
|
||||
"me": "Tu",
|
||||
"password_updated": "Password modificata.",
|
||||
"activate_user": "Confermato",
|
||||
"displayname": "Nome mostrato",
|
||||
"federation": "Federazione",
|
||||
"set_password": "Imposta password",
|
||||
"copy_link": "Copia link",
|
||||
"send_via_mail": "Invia e-mail",
|
||||
"add_to_calendar": "Aggiungi al calendario",
|
||||
"instances": "Istanze",
|
||||
"copied": "Copiato",
|
||||
"embed": "Incorpora",
|
||||
"embed_title": "Mostra questo evento sul tuo sito web",
|
||||
"embed_help": "Copiando il seguente codice sul tuo sito web l'evento verrà incluso come qui di lato",
|
||||
"feed": "Feed RSS",
|
||||
"feed_url_copied": "URL copiato, incollalo nel tuo lettore di Feed RSS",
|
||||
"follow_me_title": "Segui gli aggiornamenti dal fediverso",
|
||||
"follow": "Segui",
|
||||
"n_resources": "nessuna risorsa|una risorsa|{n} risorse",
|
||||
"resources": "Risorse",
|
||||
"moderation": "Moderazione",
|
||||
"authorize": "Autorizza",
|
||||
"title": "Titolo",
|
||||
"user": "Utente",
|
||||
"filter": "Filtra",
|
||||
"event": "Evento",
|
||||
"pause": "Pausa",
|
||||
"start": "Avvia",
|
||||
"fediverse": "Fediverso",
|
||||
"skip": "Salta",
|
||||
"delete": "Elimina",
|
||||
"announcements": "Annunci",
|
||||
"url": "URL",
|
||||
"place": "Luogo",
|
||||
"tags": "Etichette",
|
||||
"theme": "Tema",
|
||||
"reset": "Reset",
|
||||
"import": "Importa",
|
||||
"max_events": "N. massimo eventi",
|
||||
"label": "Etichetta",
|
||||
"collections": "Bolle"
|
||||
},
|
||||
"login": {
|
||||
"description": "Entrando puoi pubblicare nuovi eventi.",
|
||||
"check_email": "Controlla la tua posta (anche lo spam).",
|
||||
"not_registered": "Non sei registrata?",
|
||||
"forgot_password": "Dimenticato la password?",
|
||||
"error": "Errore durante il login, controlla i dati.",
|
||||
"insert_email": "Inserisci l'indirizzo e-mail",
|
||||
"ok": "Tutto rego"
|
||||
},
|
||||
"recover": {
|
||||
"not_valid_code": "Mmmmm qualcosa è andato storto."
|
||||
},
|
||||
"export": {
|
||||
"intro": "Contrariamente alle piattaforme del capitalismo, che fanno di tutto per tenere i dati e gli utenti al loro interno, crediamo che le informazioni, come le persone, debbano essere libere. Per questo puoi rimanere aggiornata sugli eventi che vuoi, come meglio credi, senza necessariamente passare da questo sito.",
|
||||
"email_description": "Puoi ricevere via posta elettronica gli eventi che ti interessano.",
|
||||
"insert_your_address": "Inserisci il tuo indirizzo e-mail",
|
||||
"feed_description": "Per seguire gli aggiornamenti da computer o smartphone senza la necessità di aprire periodicamente il sito, il metodo consigliato è quello dei Feed RSS.</p>\n\n <p>Con i feed rss utilizzi un'apposita applicazione per ricevere aggiornamenti dai siti che più ti interessano. È un buon metodo per seguire anche molti siti in modo molto rapido, senza necessità di creare un account o altre complicazioni.</p>\n\n<li>Se hai Android, ti consigliamo <a href=\"https://f-droid.org/en/packages/net.frju.flym\">Flym</a> o Feeder</li>\n<li>Per iPhone/iPad puoi usare <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\">Feed4U</a></li>\n<li>Per il computer fisso/portatile consigliamo Brief, da installare all'interno <a href=\"https://addons.mozilla.org/it/firefox/addon/brief/\">di Firefox </a> e compatibile con tutti i principali sistemi operativi.</li>\n<br/>\nAggiungendo questo link al tuo lettore di feed, rimarrai aggiornata.",
|
||||
"ical_description": "I computer e gli smartphone sono comunemente attrezzati con un'applicazione per gestire un calendario. A questi programmi solitamente è possibile far importare un calendario remoto.",
|
||||
"list_description": "Se hai un sito web e vuoi mostrare una lista di eventi, puoi usare il seguente codice"
|
||||
},
|
||||
"register": {
|
||||
"description": "I movimenti hanno bisogno di organizzarsi e autofinanziarsi. <br/>\nUsatelo solo per eventi non commerciali, antifascisti, antisessisti, antirazzisti. <br/>Prima di poter pubblicare <strong>dobbiamo approvare l'account</strong>, considera che <strong>dietro questo sito ci sono delle persone</strong> di carne e sangue, scrivici quindi due righe per farci capire che eventi vorresti pubblicare.",
|
||||
"error": "Errore: ",
|
||||
"complete": "Confermeremo la registrazione quanto prima.",
|
||||
"first_user": "Amministratore creato e attivo"
|
||||
},
|
||||
"event": {
|
||||
"anon": "Anonimo",
|
||||
"anon_description": "Puoi inserire un evento senza registrarti o fare il login, ma in questo caso dovrai aspettare che qualcuno lo legga confermando che si tratta di un evento adatto a questo spazio, delegando questa scelta. Inoltre non sarà possibile modificarlo.<br/><br/> \nPuoi invece fare il <a href='/login'>login</a> o <a href='/register'>registrarti</a>, altrimenti vai avanti, lo confermeremo appena possibile. ",
|
||||
"same_day": "Stesso giorno",
|
||||
"what_description": "Titolo",
|
||||
"description_description": "Descrizione",
|
||||
"tag_description": "Etichetta",
|
||||
"media_description": "Puoi aggiungere un volantino (opzionale)",
|
||||
"added": "Evento aggiunto",
|
||||
"saved": "Evento salvato",
|
||||
"added_anon": "Evento aggiunto, verrà confermato quanto prima.",
|
||||
"updated": "Evento aggiornato",
|
||||
"where_description": "Dov'è il gancio? Se il posto non è presente potrai crearlo.",
|
||||
"confirmed": "Evento confermato",
|
||||
"not_found": "Evento non trovato",
|
||||
"remove_confirmation": "Vuoi eliminare questo evento?",
|
||||
"remove_recurrent_confirmation": "Sei sicura di voler eliminare questo evento ricorrente?\nGli eventi passati verranno mantenuti ma non ne verranno creati altri.",
|
||||
"recurrent": "Ricorrente",
|
||||
"edit_recurrent": "Modifica evento ricorrente:",
|
||||
"show_recurrent": "appuntamenti ricorrenti",
|
||||
"show_past": "eventi passati",
|
||||
"recurrent_description": "Scegli la frequenza e seleziona i giorni",
|
||||
"multidate_description": "Un festival o una tre giorni? Scegli quando comincia e quando finisce",
|
||||
"multidate": "Più giorni",
|
||||
"normal": "Normale",
|
||||
"normal_description": "Scegli il giorno.",
|
||||
"recurrent_1w_days": "Ogni {days}",
|
||||
"recurrent_2w_days": "Un {days} ogni due",
|
||||
"recurrent_1m_days": "Il giorno {days} di ogni mese",
|
||||
"recurrent_2m_days": "Il giorno {days} ogni due mesi",
|
||||
"recurrent_1m_ordinal": "Il {n} {days} di ogni mese",
|
||||
"recurrent_2m_ordinal": "Il {n} {days} un mese sì e uno no",
|
||||
"each_week": "Ogni settimana",
|
||||
"each_2w": "Ogni due settimane",
|
||||
"each_month": "Ogni mese",
|
||||
"due": "alle",
|
||||
"from": "Dalle",
|
||||
"image_too_big": "L'immagine non può essere più grande di 4MB",
|
||||
"interact_with_me": "Seguimi dal fediverso",
|
||||
"follow_me_description": "Tra i vari modi di rimanere aggiornati degli eventi pubblicati qui su {title},\npuoi seguire l'account <u>{account}</u> dal fediverso, ad esempio via Mastodon, ed eventualmente aggiungere risorse ad un evento da lì.<br/><br/>\nSe non hai mai sentito parlare di Mastodon e del fediverso ti consigliamo di leggere <a href='https://cagizero.wordpress.com/2018/10/25/cose-mastodon/'>questo articolo</a>.<br/><br/> Inserisci la tua istanza qui sotto (es. mastodon.cisti.org o mastodon.bida.im)",
|
||||
"only_future": "solo eventi futuri",
|
||||
"interact_with_me_at": "Seguimi nel fediverso su",
|
||||
"import_ICS": "Importa da ICS",
|
||||
"import_URL": "Importa da URL (ics o h-event)",
|
||||
"ics": "ICS",
|
||||
"import_description": "Puoi importare eventi da altre piattaforme e da altre istanze attraverso i formati standard (ics e h-event)",
|
||||
"alt_text_description": "Descrizione per persone con disabilità visive",
|
||||
"choose_focal_point": "Scegli il punto centrale cliccando",
|
||||
"remove_media_confirmation": "Confermi l'eliminazione dell'immagine?",
|
||||
"download_flyer": "Scarica volantino"
|
||||
},
|
||||
"admin": {
|
||||
"place_description": "Nel caso in cui un luogo sia errato o cambi indirizzo, puoi modificarlo.<br/>Considera che tutti gli eventi associati a questo luogo cambieranno indirizzo (anche quelli passati).",
|
||||
"event_confirm_description": "Puoi confermare qui gli eventi inseriti da utenti anonimi",
|
||||
"delete_user": "Elimina",
|
||||
"remove_admin": "Rimuovi admin",
|
||||
"disable_user_confirm": "Vuoi disabilitare {user}?",
|
||||
"delete_user_confirm": "Vuoi rimuovere {user}?",
|
||||
"user_remove_ok": "Utente eliminato",
|
||||
"user_create_ok": "Utente creato",
|
||||
"allow_registration_description": "Vuoi abilitare la registrazione?",
|
||||
"allow_anon_event": "Si possono inserire eventi anonimi (previa conferma)?",
|
||||
"allow_recurrent_event": "Abilita eventi ricorrenti",
|
||||
"recurrent_event_visible": "Appuntamenti ricorrenti visibili di default",
|
||||
"federation": "Federazione / ActivityPub",
|
||||
"enable_federation": "Abilita la federazione",
|
||||
"enable_federation_help": "Sarà possibile seguire questa istanza dal fediverso",
|
||||
"add_instance": "Aggiungi istanza",
|
||||
"select_instance_timezone": "Fuso orario",
|
||||
"instance_timezone_description": "Gancio è pensato per raccogliere gli eventi di un luogo specifico come ad esempio una città. Scrivendo e selezionando il fuso orario di questo luogo, tutti gli orari saranno mostrati e inseriti secondo quanto scelto.",
|
||||
"enable_resources": "Abilita risorse",
|
||||
"enable_resources_help": "Permette di aggiungere risorse agli eventi dal fediverso",
|
||||
"hide_boost_bookmark": "Nasconde n. condivisioni e segnalibri",
|
||||
"hide_boost_bookmark_help": "Nasconde le piccole icone che mostrano il numero di boost e segnalibri in arrivo dal fediverso",
|
||||
"block": "Blocca",
|
||||
"unblock": "Sblocca",
|
||||
"user_add_help": "Manderemo un'e-mail al nuovo utente con le istruzioni per confermare l'iscrizione e scegliere una password",
|
||||
"resources": "Risorse",
|
||||
"hide_resource": "Nascondi risorsa",
|
||||
"show_resource": "Mostra risorsa",
|
||||
"delete_resource": "Elimina risorsa",
|
||||
"delete_resource_confirm": "Sei sicurǝ di voler eliminare questa risorsa?",
|
||||
"block_user": "Blocca questo utente",
|
||||
"user_blocked": "L'utente {user} non potrà più aggiungere risorse",
|
||||
"filter_instances": "Filtra istanze",
|
||||
"filter_users": "Filtra utenti",
|
||||
"instance_name": "Nome istanza",
|
||||
"favicon": "Logo",
|
||||
"user_block_confirm": "Confermi di voler bloccare l'utente {user}?",
|
||||
"instance_block_confirm": "Confermi di voler bloccare l'istanza {instance}?",
|
||||
"delete_announcement_confirm": "Vuoi eliminare questo l'annuncio?",
|
||||
"announcement_remove_ok": "Annuncio rimosso",
|
||||
"announcement_description": "In questa sezione puoi inserire annunci che rimarranno in homepage",
|
||||
"instance_locale": "Lingua predefinita",
|
||||
"instance_locale_description": "La lingua utilizzata per mostrare le pagine è quella preferita dall'utente. In alcuni casi però dobbiamo mostrare dei messaggi per tutti allo stesso modo (ad esempio quando pubblichiamo via ActivityPub o nell'invio di alcune e-mail). In questi casi useremo la lingua selezionata qui sopra.",
|
||||
"instance_place": "Luogo indicativo di questa istanza",
|
||||
"title_description": "Viene usato nel titolo della pagina, nell'oggetto delle e-mail, nell'esportazione dei flussi RSS e degli ICS.",
|
||||
"description_description": "Compare nell'intestazione accanto al titolo",
|
||||
"instance_name_help": "Nome dell'account ActivityPub da seguire",
|
||||
"enable_trusted_instances": "Abilita istanze amiche",
|
||||
"trusted_instances_help": "Le istanze amiche compariranno nella barra di navigazione in cima alla pagina",
|
||||
"add_trusted_instance": "Aggiungi un'istanza amica",
|
||||
"instance_place_help": "Verrà mostrata questa stringa nel menù delle altre istanze amiche",
|
||||
"delete_trusted_instance_confirm": "Vuoi davvero eliminare questa voce dal menù delle istanze amiche?",
|
||||
"is_dark": "Tema scuro",
|
||||
"add_link": "Aggiungi link",
|
||||
"footer_links": "Collegamenti del piè di pagina",
|
||||
"delete_footer_link_confirm": "Vuoi eliminare questo collegamento?",
|
||||
"edit_place": "Modifica luogo",
|
||||
"new_announcement": "Nuovo annuncio",
|
||||
"show_smtp_setup": "Impostazioni email",
|
||||
"widget": "Widget",
|
||||
"smtp_description": "<ul><li>L'amministratore riceve una email quando viene aggiunto un evento anonimo (se abilitati).</li><li>L'amministratore riceve le mail di richiesta di registrazione (se abilitata).</li><li>Le persone ricevono una mail di conferma della registrazione richiesta.</li><li>Le persone ricevono le email della registrazione confermata.</li><li>Le persone ricevono una mail di avviso quando vengono registrate direttamente da admin.</li><li>Le persone ricevono la mail per modificare la password quando l'hanno dimenticata</li></ul>",
|
||||
"smtp_hostname": "SMTP Hostname",
|
||||
"smtp_test_success": "Una mail di test è stata inviata all'indirizzo {admin_email}, controlla la tua casella di posta",
|
||||
"smtp_test_button": "Invia una mail di prova",
|
||||
"admin_email": "E-mail dell'admin",
|
||||
"new_collection": "Crea bolla",
|
||||
"wrong_domain_warning": "Il \"baseurl\" configurato in config.json <b>({baseurl})</b> è diverso da quello che stai visitando <b>({url})</b>",
|
||||
"collections_description": "Le bolle sono raggruppamenti di eventi per tag e posti.",
|
||||
"edit_collection": "Modifica bolla"
|
||||
},
|
||||
"auth": {
|
||||
"not_confirmed": "Non ancora confermato…",
|
||||
"fail": "Autenticazione fallita. Sicura la password è giusta? E l'e-mail?"
|
||||
},
|
||||
"settings": {
|
||||
"update_confirm": "Vuoi salvare le modifiche?",
|
||||
"change_password": "Cambia password",
|
||||
"password_updated": "Password modificata.",
|
||||
"danger_section": "Sezione pericolosa",
|
||||
"remove_account": "Premendo il seguente tasto il tuo utente verrà eliminato. Gli eventi da te pubblicati invece no.",
|
||||
"remove_account_confirm": "Stai per eliminare definitivamente il tuo account"
|
||||
},
|
||||
"error": {
|
||||
"nick_taken": "Questo nome utente è già presente.",
|
||||
"email_taken": "Questa e-mail è già registrata."
|
||||
},
|
||||
"confirm": {
|
||||
"title": "Conferma utente",
|
||||
"not_valid": "Qualcosa è andato storto.",
|
||||
"valid": "Il tuo account è stato confermato, ora puoi <a href=\"/login\">entrare</a>"
|
||||
},
|
||||
"ordinal": {
|
||||
"1": "primo",
|
||||
"2": "secondo",
|
||||
"3": "terzo",
|
||||
"4": "quarto",
|
||||
"5": "quinto",
|
||||
"-1": "ultimo"
|
||||
},
|
||||
"validators": {
|
||||
"required": "Campo {fieldName} necessario",
|
||||
"email": "Inserisci un'e-mail valida"
|
||||
},
|
||||
"about": "\n <p> <a href='https://gancio.org'> Gancio </a> è un'agenda condivisa per le comunità locali. </p>\n ",
|
||||
"oauth": {
|
||||
"authorization_request": "L'applicazione esterna <code>{app}</code> richiede l'autorizzazione a svolgere le sequenti attività su <code>{instance_name}</code>:",
|
||||
"redirected_to": "Dopo la conferma sarai reindirizzata all'indirizzo <code>{url}</code>",
|
||||
"scopes": {
|
||||
"event:write": "Pubblicare/modificare i tuoi eventi"
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
"completed": "Setup completato",
|
||||
"completed_description": "<p>Puoi entrare con le seguenti credenziali:<br/><br/>Utente: <b>{email}</b><br/>Password: <b>{password}<b/></p>",
|
||||
"copy_password_dialog": "Sì, devi copiare la password!",
|
||||
"start": "Inizia",
|
||||
"https_warning": "Stai visitando il setup da HTTP, ricorda di cambiare il baseurl nel config.json quando passerai ad HTTPS!"
|
||||
"common": {
|
||||
"add_event": "Aggiungi evento",
|
||||
"next": "Continua",
|
||||
"export": "Esporta",
|
||||
"send": "Invia",
|
||||
"where": "Dove",
|
||||
"address": "Indirizzo",
|
||||
"when": "Quando",
|
||||
"what": "Cosa",
|
||||
"media": "Media",
|
||||
"login": "Entra",
|
||||
"email": "E-mail",
|
||||
"password": "Password",
|
||||
"register": "Registrati",
|
||||
"description": "Descrizione",
|
||||
"remove": "Elimina",
|
||||
"hide": "Nascondi",
|
||||
"search": "Cerca",
|
||||
"edit": "Modifica",
|
||||
"info": "Info",
|
||||
"confirm": "Conferma",
|
||||
"admin": "Amministra",
|
||||
"users": "Utenti",
|
||||
"events": "Eventi",
|
||||
"places": "Luoghi",
|
||||
"settings": "Opzioni",
|
||||
"actions": "Azioni",
|
||||
"deactivate": "Disattiva",
|
||||
"remove_admin": "Rimuovi Admin",
|
||||
"activate": "Attiva",
|
||||
"save": "Salva",
|
||||
"preview": "Anteprima",
|
||||
"logout": "Esci",
|
||||
"share": "Esporta",
|
||||
"name": "Nome",
|
||||
"associate": "Associa",
|
||||
"edit_event": "Modifica evento",
|
||||
"related": "Memoria storica",
|
||||
"add": "Aggiungi",
|
||||
"logout_ok": "Uscita correttamente",
|
||||
"copy": "Copia",
|
||||
"recover_password": "Recupera password",
|
||||
"new_password": "Nuova password",
|
||||
"new_user": "Nuovo utente",
|
||||
"ok": "OK",
|
||||
"cancel": "Annulla",
|
||||
"enable": "Abilita",
|
||||
"disable": "Disabilita",
|
||||
"me": "Tu",
|
||||
"password_updated": "Password modificata.",
|
||||
"activate_user": "Confermato",
|
||||
"displayname": "Nome mostrato",
|
||||
"federation": "Federazione",
|
||||
"set_password": "Imposta password",
|
||||
"copy_link": "Copia link",
|
||||
"send_via_mail": "Invia e-mail",
|
||||
"add_to_calendar": "Aggiungi al calendario",
|
||||
"instances": "Istanze",
|
||||
"copied": "Copiato",
|
||||
"embed": "Incorpora",
|
||||
"embed_title": "Mostra questo evento sul tuo sito web",
|
||||
"embed_help": "Copiando il seguente codice sul tuo sito web l'evento verrà incluso come qui di lato",
|
||||
"feed": "Feed RSS",
|
||||
"feed_url_copied": "URL copiato, incollalo nel tuo lettore di Feed RSS",
|
||||
"follow_me_title": "Segui gli aggiornamenti dal fediverso",
|
||||
"follow": "Segui",
|
||||
"n_resources": "nessuna risorsa|una risorsa|{n} risorse",
|
||||
"resources": "Risorse",
|
||||
"moderation": "Moderazione",
|
||||
"authorize": "Autorizza",
|
||||
"title": "Titolo",
|
||||
"user": "Utente",
|
||||
"filter": "Filtra",
|
||||
"event": "Evento",
|
||||
"pause": "Pausa",
|
||||
"start": "Avvia",
|
||||
"fediverse": "Fediverso",
|
||||
"skip": "Salta",
|
||||
"delete": "Elimina",
|
||||
"announcements": "Annunci",
|
||||
"url": "URL",
|
||||
"place": "Luogo",
|
||||
"tags": "Etichette",
|
||||
"theme": "Tema",
|
||||
"reset": "Reset",
|
||||
"import": "Importa",
|
||||
"max_events": "N. massimo eventi",
|
||||
"label": "Etichetta",
|
||||
"collections": "Bolle",
|
||||
"plugins": "Plugin",
|
||||
"help_translate": "Aiuta a tradurre",
|
||||
"show_map": "Mostra mappa",
|
||||
"latitude": "Latitudine",
|
||||
"longitude": "Longitudine",
|
||||
"getting_there": "Come arrivare",
|
||||
"calendar": "Calendario",
|
||||
"home": "Home",
|
||||
"about": "Cos'è"
|
||||
},
|
||||
"login": {
|
||||
"description": "Entrando puoi pubblicare nuovi eventi.",
|
||||
"check_email": "Controlla la tua posta (anche lo spam).",
|
||||
"not_registered": "Non sei registrata?",
|
||||
"forgot_password": "Dimenticato la password?",
|
||||
"error": "Errore durante il login, controlla i dati.",
|
||||
"insert_email": "Inserisci l'indirizzo e-mail",
|
||||
"ok": "Tutto rego"
|
||||
},
|
||||
"recover": {
|
||||
"not_valid_code": "Mmmmm qualcosa è andato storto."
|
||||
},
|
||||
"export": {
|
||||
"intro": "Contrariamente alle piattaforme del capitalismo, che fanno di tutto per tenere i dati e gli utenti al loro interno, crediamo che le informazioni, come le persone, debbano essere libere. Per questo puoi rimanere aggiornata sugli eventi che vuoi, come meglio credi, senza necessariamente passare da questo sito.",
|
||||
"email_description": "Puoi ricevere via posta elettronica gli eventi che ti interessano.",
|
||||
"insert_your_address": "Inserisci il tuo indirizzo e-mail",
|
||||
"feed_description": "Per seguire gli aggiornamenti da computer o smartphone senza la necessità di aprire periodicamente il sito, il metodo consigliato è quello dei Feed RSS.</p>\n\n <p>Con i feed rss utilizzi un'apposita applicazione per ricevere aggiornamenti dai siti che più ti interessano. È un buon metodo per seguire anche molti siti in modo molto rapido, senza necessità di creare un account o altre complicazioni.</p>\n\n<li>Se hai Android, ti consigliamo <a href=\"https://f-droid.org/en/packages/net.frju.flym\">Flym</a> o Feeder</li>\n<li>Per iPhone/iPad puoi usare <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\">Feed4U</a></li>\n<li>Per il computer fisso/portatile consigliamo Brief, da installare all'interno <a href=\"https://addons.mozilla.org/it/firefox/addon/brief/\">di Firefox </a> e compatibile con tutti i principali sistemi operativi.</li>\n<br/>\nAggiungendo questo link al tuo lettore di feed, rimarrai aggiornata.",
|
||||
"ical_description": "I computer e gli smartphone sono comunemente attrezzati con un'applicazione per gestire un calendario. A questi programmi solitamente è possibile far importare un calendario remoto.",
|
||||
"list_description": "Se hai un sito web e vuoi mostrare una lista di eventi, puoi usare il seguente codice"
|
||||
},
|
||||
"register": {
|
||||
"description": "I movimenti hanno bisogno di organizzarsi e autofinanziarsi. <br/>\nUsatelo solo per eventi non commerciali, antifascisti, antisessisti, antirazzisti. <br/>Prima di poter pubblicare <strong>dobbiamo approvare l'account</strong>, considera che <strong>dietro questo sito ci sono delle persone</strong> di carne e sangue, scrivici quindi due righe per farci capire che eventi vorresti pubblicare.",
|
||||
"error": "Errore: ",
|
||||
"complete": "Confermeremo la registrazione quanto prima.",
|
||||
"first_user": "Amministratore creato e attivo"
|
||||
},
|
||||
"event": {
|
||||
"anon": "Anonimo",
|
||||
"anon_description": "Puoi inserire un evento senza registrarti o fare il login, ma in questo caso dovrai aspettare che qualcuno lo legga confermando che si tratta di un evento adatto a questo spazio, delegando questa scelta. Inoltre non sarà possibile modificarlo.<br/><br/> \nPuoi invece fare il <a href='/login'>login</a> o <a href='/register'>registrarti</a>, altrimenti vai avanti, lo confermeremo appena possibile. ",
|
||||
"same_day": "Stesso giorno",
|
||||
"what_description": "Titolo",
|
||||
"description_description": "Descrizione",
|
||||
"tag_description": "Etichetta",
|
||||
"media_description": "Puoi aggiungere un volantino (opzionale)",
|
||||
"added": "Evento aggiunto",
|
||||
"saved": "Evento salvato",
|
||||
"added_anon": "Evento aggiunto, verrà confermato quanto prima.",
|
||||
"updated": "Evento aggiornato",
|
||||
"where_description": "Dov'è il gancio? Se il posto non è presente potrai crearlo.",
|
||||
"address_description": "A che indirizzo?",
|
||||
"address_description_osm": "A che indirizzo? (<a href='http://osm.org/copyright'>OpenStreetMap</a>)",
|
||||
"coordinates_search_description": "Puoi ricercare il posto per nome, o incollare la coppia di coordinate.",
|
||||
"confirmed": "Evento confermato",
|
||||
"not_found": "Evento non trovato",
|
||||
"remove_confirmation": "Vuoi eliminare questo evento?",
|
||||
"remove_recurrent_confirmation": "Sei sicura di voler eliminare questo evento ricorrente?\nGli eventi passati verranno mantenuti ma non ne verranno creati altri.",
|
||||
"recurrent": "Ricorrente",
|
||||
"edit_recurrent": "Modifica evento ricorrente:",
|
||||
"show_recurrent": "appuntamenti ricorrenti",
|
||||
"show_past": "eventi passati",
|
||||
"recurrent_description": "Scegli la frequenza e seleziona i giorni",
|
||||
"multidate_description": "Un festival o una tre giorni? Scegli quando comincia e quando finisce",
|
||||
"multidate": "Più giorni",
|
||||
"normal": "Normale",
|
||||
"normal_description": "Scegli il giorno.",
|
||||
"recurrent_1w_days": "Ogni {days}",
|
||||
"recurrent_2w_days": "Un {days} ogni due",
|
||||
"recurrent_1m_days": "Il giorno {days} di ogni mese",
|
||||
"recurrent_2m_days": "Il giorno {days} ogni due mesi",
|
||||
"recurrent_1m_ordinal": "Il {n} {days} di ogni mese",
|
||||
"recurrent_2m_ordinal": "Il {n} {days} un mese sì e uno no",
|
||||
"each_week": "Ogni settimana",
|
||||
"each_2w": "Ogni due settimane",
|
||||
"each_month": "Ogni mese",
|
||||
"due": "alle",
|
||||
"from": "Dalle",
|
||||
"image_too_big": "L'immagine non può essere più grande di 4MB",
|
||||
"interact_with_me": "Seguimi dal fediverso",
|
||||
"follow_me_description": "Tra i vari modi di rimanere aggiornati degli eventi pubblicati qui su {title},\npuoi seguire l'account <u>{account}</u> dal fediverso, ad esempio via Mastodon, ed eventualmente aggiungere risorse ad un evento da lì.<br/><br/>\nSe non hai mai sentito parlare di Mastodon e del fediverso ti consigliamo di leggere <a href='https://cagizero.wordpress.com/2018/10/25/cose-mastodon/'>questo articolo</a>.<br/><br/> Inserisci la tua istanza qui sotto (es. mastodon.cisti.org o mastodon.bida.im)",
|
||||
"only_future": "solo eventi futuri",
|
||||
"interact_with_me_at": "Seguimi nel fediverso su",
|
||||
"import_ICS": "Importa da ICS",
|
||||
"import_URL": "Importa da URL (ics o h-event)",
|
||||
"ics": "ICS",
|
||||
"import_description": "Puoi importare eventi da altre piattaforme e da altre istanze attraverso i formati standard (ics e h-event)",
|
||||
"alt_text_description": "Descrizione per persone con disabilità visive",
|
||||
"choose_focal_point": "Scegli il punto centrale cliccando",
|
||||
"remove_media_confirmation": "Confermi l'eliminazione dell'immagine?",
|
||||
"download_flyer": "Scarica volantino"
|
||||
},
|
||||
"admin": {
|
||||
"place_description": "Nel caso in cui un luogo sia errato o cambi indirizzo, puoi modificarlo.<br/>Considera che tutti gli eventi associati a questo luogo cambieranno indirizzo (anche quelli passati).",
|
||||
"event_confirm_description": "Puoi confermare qui gli eventi inseriti da utenti anonimi",
|
||||
"delete_user": "Elimina",
|
||||
"remove_admin": "Rimuovi admin",
|
||||
"disable_user_confirm": "Vuoi disabilitare {user}?",
|
||||
"delete_user_confirm": "Vuoi rimuovere {user}?",
|
||||
"user_remove_ok": "Utente eliminato",
|
||||
"user_create_ok": "Utente creato",
|
||||
"allow_registration_description": "Vuoi abilitare la registrazione?",
|
||||
"allow_anon_event": "Si possono inserire eventi anonimi (previa conferma)?",
|
||||
"allow_recurrent_event": "Abilita eventi ricorrenti",
|
||||
"allow_geolocation": "Abilita la geolocalizzazione degli eventi",
|
||||
"recurrent_event_visible": "Appuntamenti ricorrenti visibili di default",
|
||||
"federation": "Federazione / ActivityPub",
|
||||
"enable_federation": "Abilita la federazione",
|
||||
"enable_federation_help": "Sarà possibile seguire questa istanza dal fediverso",
|
||||
"add_instance": "Aggiungi istanza",
|
||||
"select_instance_timezone": "Fuso orario",
|
||||
"instance_timezone_description": "Gancio è pensato per raccogliere gli eventi di un luogo specifico come ad esempio una città. Scrivendo e selezionando il fuso orario di questo luogo, tutti gli orari saranno mostrati e inseriti secondo quanto scelto.",
|
||||
"enable_resources": "Abilita risorse",
|
||||
"enable_resources_help": "Permette di aggiungere risorse agli eventi dal fediverso",
|
||||
"hide_boost_bookmark": "Nasconde n. condivisioni e segnalibri",
|
||||
"hide_boost_bookmark_help": "Nasconde le piccole icone che mostrano il numero di boost e segnalibri in arrivo dal fediverso",
|
||||
"block": "Blocca",
|
||||
"unblock": "Sblocca",
|
||||
"user_add_help": "Manderemo un'e-mail al nuovo utente con le istruzioni per confermare l'iscrizione e scegliere una password",
|
||||
"resources": "Risorse",
|
||||
"hide_resource": "Nascondi risorsa",
|
||||
"show_resource": "Mostra risorsa",
|
||||
"delete_resource": "Elimina risorsa",
|
||||
"delete_resource_confirm": "Sei sicurǝ di voler eliminare questa risorsa?",
|
||||
"block_user": "Blocca questo utente",
|
||||
"user_blocked": "L'utente {user} non potrà più aggiungere risorse",
|
||||
"filter_instances": "Filtra istanze",
|
||||
"filter_users": "Filtra utenti",
|
||||
"instance_name": "Nome istanza",
|
||||
"favicon": "Logo",
|
||||
"user_block_confirm": "Confermi di voler bloccare l'utente {user}?",
|
||||
"instance_block_confirm": "Confermi di voler bloccare l'istanza {instance}?",
|
||||
"delete_announcement_confirm": "Vuoi eliminare questo l'annuncio?",
|
||||
"announcement_remove_ok": "Annuncio rimosso",
|
||||
"announcement_description": "In questa sezione puoi inserire annunci che rimarranno in homepage",
|
||||
"instance_locale": "Lingua predefinita",
|
||||
"instance_locale_description": "La lingua utilizzata per mostrare le pagine è quella preferita dall'utente. In alcuni casi però dobbiamo mostrare dei messaggi per tutti allo stesso modo (ad esempio quando pubblichiamo via ActivityPub o nell'invio di alcune e-mail). In questi casi useremo la lingua selezionata qui sopra.",
|
||||
"instance_place": "Luogo indicativo di questa istanza",
|
||||
"title_description": "Viene usato nel titolo della pagina, nell'oggetto delle e-mail, nell'esportazione dei flussi RSS e degli ICS.",
|
||||
"description_description": "Compare nell'intestazione accanto al titolo",
|
||||
"instance_name_help": "Nome dell'account ActivityPub da seguire",
|
||||
"enable_trusted_instances": "Abilita istanze amiche",
|
||||
"trusted_instances_help": "Le istanze amiche compariranno nella barra di navigazione in cima alla pagina",
|
||||
"trusted_instances_label": "Etichetta di navigazione alle istanze amiche",
|
||||
"trusted_instances_label_default": "Istanze amiche",
|
||||
"trusted_instances_label_help": "L'etichetta di default è 'Istanze amiche'",
|
||||
"add_trusted_instance": "Aggiungi un'istanza amica",
|
||||
"instance_place_help": "Verrà mostrata questa stringa nel menù delle altre istanze amiche",
|
||||
"delete_trusted_instance_confirm": "Vuoi davvero eliminare questa voce dal menù delle istanze amiche?",
|
||||
"is_dark": "Tema scuro",
|
||||
"add_link": "Aggiungi link",
|
||||
"footer_links": "Collegamenti del piè di pagina",
|
||||
"delete_footer_link_confirm": "Vuoi eliminare questo collegamento?",
|
||||
"edit_place": "Modifica luogo",
|
||||
"edit_tag": "Modifica tag",
|
||||
"edit_tag_help": "Puoi cambiare il tag sostituendolo con uno nuovo o unendolo ad uno gia' esistente. Verranno modificati anche i {n} eventi associati",
|
||||
"new_announcement": "Nuovo annuncio",
|
||||
"show_smtp_setup": "Impostazioni email",
|
||||
"widget": "Widget",
|
||||
"smtp_description": "<ul><li>L'amministratore riceve una email quando viene aggiunto un evento anonimo (se abilitati).</li><li>L'amministratore riceve le mail di richiesta di registrazione (se abilitata).</li><li>Le persone ricevono una mail di conferma della registrazione richiesta.</li><li>Le persone ricevono le email della registrazione confermata.</li><li>Le persone ricevono una mail di avviso quando vengono registrate direttamente da admin.</li><li>Le persone ricevono la mail per modificare la password quando l'hanno dimenticata</li></ul>",
|
||||
"smtp_hostname": "SMTP Hostname",
|
||||
"smtp_test_success": "Una mail di test è stata inviata all'indirizzo {admin_email}, controlla la tua casella di posta",
|
||||
"smtp_test_button": "Invia una mail di prova",
|
||||
"admin_email": "E-mail dell'admin",
|
||||
"admin_email_help": "L'indirizzo che usiamo come mittente per inviare le e-mail. È anche l'indirizzo a cui vengono spedite le e-mail di amministrazione",
|
||||
"new_collection": "Crea bolla",
|
||||
"wrong_domain_warning": "Il \"baseurl\" configurato in config.json <b>({baseurl})</b> è diverso da quello che stai visitando <b>({url})</b>",
|
||||
"collections_description": "Le bolle sono raggruppamenti di eventi per tag e posti.",
|
||||
"edit_collection": "Modifica bolla",
|
||||
"config_plugin": "Configura plugin",
|
||||
"fallback_image": "Immagine di ripiego",
|
||||
"header_image": "Immagine di intestazione",
|
||||
"hide_thumbs": "Nascondi immaginine",
|
||||
"hide_calendar": "Nascondi calendario",
|
||||
"default_images": "Immagini preimpostate",
|
||||
"default_images_help": "Devi <a href='/admin?tab=theme'>aggiornare</a> la pagina per vedere le modifiche.",
|
||||
"blocked": "Bloccato",
|
||||
"domain": "Domini",
|
||||
"known_users": "Utenti conosciuti",
|
||||
"created_at": "Creato il",
|
||||
"geolocation_description": "<b>1. Definisci un fornitore per il servizio di georeferenziazione (geocodifica)</b>.<br>Al momento, tra quelli elencati nella <a href=\"https://wiki.openstreetmap.org/wiki/Nominatim#Alternatives_.2F_Third-party_providers\">wiki di OpenStreetMap</a>, è presente il supporto per i software <a href=\"https://github.com/osm-search/Nominatim\">Nominatim</a> e <a href=\"https://github.com/komoot/photon\">Photon</a>.<br>Puoi utilizzare una delle relative demo ufficiali copiandone il link nel campo 'Fornitore georeferenziazione':<ul><li>https://nominatim.openstreetmap.org/search (<a href=\"https://operations.osmfoundation.org/policies/nominatim/\">Terms of Service</a>)</li><li>https://photon.komoot.io/api/ (<a href=\"https://photon.komoot.io/\">Terms of Service</a>)</li></ul><br><b>2. Definisci un fornitore di layers per la mappa.</b><br>Qui puoi trovarne una lista: <a href=\"https://leaflet-extras.github.io/leaflet-providers/preview/\">https://leaflet-extras.github.io/leaflet-providers/preview/</a>",
|
||||
"geocoding_provider_type": "Software fornitore georeferenziazione",
|
||||
"geocoding_provider_type_help": "Il software di default è Nominatim",
|
||||
"geocoding_provider": "Fornitore georeferenziazione",
|
||||
"geocoding_provider_help": "Il fornitore di default è Nominatim",
|
||||
"geocoding_countrycodes": "Codici territoriali",
|
||||
"geocoding_countrycodes_help": "Permette di impostare un filtro alle ricerche basato su codici territori nazionali",
|
||||
"geocoding_test_button": "Test geocoding",
|
||||
"geocoding_test_success": "Il servizio di geocoding all'indirizzo {service_name} sta funzionando",
|
||||
"geocoding_test_error": "Il servizio non è raggiungibile all'indirizzo: {service_name}",
|
||||
"tilelayer_provider": "Fornitore tilelayer",
|
||||
"tilelayer_provider_help": "Il fornitore di default è OpenStreetMap",
|
||||
"tilelayer_provider_attribution": "Attribuzione",
|
||||
"tilelayer_test_button": "Test tilelayer",
|
||||
"tilelayer_test_success": "Il servizio di tilelayer all'indirizzo {service_name} sta funzionando",
|
||||
"tilelayer_test_error": "Il servizio non è raggiungibile all'indirizzo: {service_name}",
|
||||
"geolocation": "Geo e mappe"
|
||||
},
|
||||
"auth": {
|
||||
"not_confirmed": "Non ancora confermato…",
|
||||
"fail": "Autenticazione fallita. Sicura la password è giusta? E l'e-mail?"
|
||||
},
|
||||
"settings": {
|
||||
"update_confirm": "Vuoi salvare le modifiche?",
|
||||
"change_password": "Cambia password",
|
||||
"password_updated": "Password modificata.",
|
||||
"danger_section": "Sezione pericolosa",
|
||||
"remove_account": "Premendo il seguente tasto il tuo utente verrà eliminato. Gli eventi da te pubblicati invece no.",
|
||||
"remove_account_confirm": "Stai per eliminare definitivamente il tuo account"
|
||||
},
|
||||
"error": {
|
||||
"nick_taken": "Questo nome utente è già presente.",
|
||||
"email_taken": "Questa e-mail è già registrata."
|
||||
},
|
||||
"confirm": {
|
||||
"title": "Conferma utente",
|
||||
"not_valid": "Qualcosa è andato storto.",
|
||||
"valid": "Il tuo account è stato confermato, ora puoi <a href=\"/login\">entrare</a>"
|
||||
},
|
||||
"ordinal": {
|
||||
"1": "primo",
|
||||
"2": "secondo",
|
||||
"3": "terzo",
|
||||
"4": "quarto",
|
||||
"5": "quinto",
|
||||
"-1": "ultimo"
|
||||
},
|
||||
"validators": {
|
||||
"required": "Campo {fieldName} necessario",
|
||||
"email": "Inserisci un'e-mail valida"
|
||||
},
|
||||
"about": "\n <p> <a href='https://gancio.org'> Gancio </a> è un'agenda condivisa per le comunità locali. </p>\n ",
|
||||
"oauth": {
|
||||
"authorization_request": "L'applicazione esterna <code>{app}</code> richiede l'autorizzazione a svolgere le sequenti attività su <code>{instance_name}</code>:",
|
||||
"redirected_to": "Dopo la conferma sarai reindirizzata all'indirizzo <code>{url}</code>",
|
||||
"scopes": {
|
||||
"event:write": "Pubblicare/modificare i tuoi eventi"
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
"completed": "Setup completato",
|
||||
"completed_description": "<p>Puoi entrare con le seguenti credenziali:<br/><br/>Utente: <b>{email}</b><br/>Password: <b>{password}<b/></p>",
|
||||
"copy_password_dialog": "Sì, devi copiare la password!",
|
||||
"start": "Inizia",
|
||||
"https_warning": "Stai visitando il setup da HTTP, ricorda di cambiare il baseurl nel config.json quando passerai ad HTTPS!"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,6 +61,9 @@
|
|||
"instance_place_help": "Etikett å vise i andres instanser",
|
||||
"add_trusted_instance": "Legg til en vennlig instans",
|
||||
"trusted_instances_help": "Liste over vennlige instanser vises i toppteksten",
|
||||
"trusted_instances_label": "Navigasjonsetikett til venneforekomster",
|
||||
"trusted_instances_label_default": "Vennlige tilfeller",
|
||||
"trusted_instances_label_help": "Standardetiketten er 'Vennlige tilfeller'",
|
||||
"enable_trusted_instances": "Skru på vennlige instanser",
|
||||
"instance_name_help": "ActivityPub-konto å følge",
|
||||
"instance_place": "Indiker sted for denne instansen",
|
||||
|
@ -81,6 +84,7 @@
|
|||
"allow_recurrent_event": "Tillat gjentagende hendelser",
|
||||
"allow_anon_event": "Tillat anonyme hendelser (må bekreftes)?",
|
||||
"allow_registration_description": "Tillat selv-registrering?",
|
||||
"allow_geolocation": "Tillat geolokalisering av hendelser",
|
||||
"event_confirm_description": "Du kan bekrefte hendelser som oppføres av anonyme brukere her",
|
||||
"place_description": "Hvis du har valgt feil sted eller adresse, kan du endre det. <br/>Alle nåværende og foregående hendelser tilknyttet dette stedet vil endre adresse."
|
||||
},
|
||||
|
@ -114,11 +118,13 @@
|
|||
"image_too_big": "Bildet kan ikke være større enn 4 MB",
|
||||
"recurrent_2m_ordinal": "|Den {n} {days} i måneden annenhver|Den {n} {days} i måneden annenhver",
|
||||
"recurrent_1m_ordinal": "På {n} {days} i hver måned",
|
||||
"recurrent_2m_days": "|På {days} i hver måned annenhver|{days} i hver måned annenhver",
|
||||
"recurrent_1m_days": "|På {days} i hver måned|{days} i hver måned",
|
||||
"recurrent_2m_days": "På {days} i hver måned annenhver",
|
||||
"recurrent_1m_days": "På {days} i hver måned",
|
||||
"recurrent_2w_days": "En {days} annenhver",
|
||||
"multidate_description": "Er det en festival? Velg når den starter og slutter",
|
||||
"where_description": "Hvor finner hendelsen sted? Hvis den ikke finnes kan du opprette den.",
|
||||
"coordinates_search": "Søk etter koordinater",
|
||||
"coordinates_search_description": "Du kan søke etter sted etter navn, eller lime inn koordinatparet.",
|
||||
"added_anon": "Hendelse lagt til, men ikke bekreftet enda.",
|
||||
"added": "Hendelse lagt til",
|
||||
"media_description": "Du kan legge til et flygeblad (valgfritt)",
|
||||
|
@ -133,7 +139,7 @@
|
|||
"first_user": "Administrator opprettet",
|
||||
"complete": "Registrering må bekreftes.",
|
||||
"error": "Feil: ",
|
||||
"description": "Sosiale bevegelser bør organisere og finansiere seg selv.<br/>\n<br/>Før du kan publisere, <strong>må kontoen godkjennes</strong>, ha i minnet <strong> at bak denne siden er det mennesker, så skriv to linjer om hvilke hendelser du ønsker å publisere."
|
||||
"description": "Sosiale bevegelser bør organisere og finansiere seg selv.<br/>\n<br/>Før du kan publisere, <strong>må kontoen godkjennes</strong>, ha i minnet <strong> at bak denne siden er det mennesker</strong>, så skriv to linjer om hvilke hendelser du ønsker å publisere."
|
||||
},
|
||||
"recover": {
|
||||
"not_valid_code": "Noe gikk galt."
|
||||
|
@ -232,7 +238,11 @@
|
|||
"federation": "Føderasjon",
|
||||
"n_resources": "ingen ressurs|én ressurs|{n} ressurser",
|
||||
"associate": "Tilknytt",
|
||||
"import": "Importer"
|
||||
"import": "Importer",
|
||||
"show_map": "Vis kart",
|
||||
"latitude": "breddegrad",
|
||||
"longitude": "Lengdegrad",
|
||||
"getting_there": "Slik kommer du deg dit"
|
||||
},
|
||||
"about": "\n <p><a href='https://gancio.org'>Gancio</a> er en delt agenda for lokale gemenskaper.</p>\n ",
|
||||
"validators": {
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
"search": "Szukaj",
|
||||
"edit": "Edytuj",
|
||||
"info": "Informacje",
|
||||
"confirm": "Potwierdź"
|
||||
"confirm": "Potwierdź",
|
||||
"admin": "Admin",
|
||||
"users": "Użytkownik"
|
||||
}
|
||||
}
|
||||
|
|
322
locales/pt.json
322
locales/pt.json
|
@ -1 +1,321 @@
|
|||
{}
|
||||
{
|
||||
"common": {
|
||||
"add_event": "Adicionar evento",
|
||||
"description": "Descrição",
|
||||
"send": "Enviar",
|
||||
"address": "Endereço",
|
||||
"next": "Próximo",
|
||||
"when": "Quando",
|
||||
"where": "Onde",
|
||||
"login": "Login",
|
||||
"export": "Exportar",
|
||||
"email": "E-mail",
|
||||
"what": "O que",
|
||||
"media": "Media",
|
||||
"password": "Senha",
|
||||
"register": "Registrar",
|
||||
"remove": "Remover",
|
||||
"confirm": "Confirmar",
|
||||
"events": "Eventos",
|
||||
"settings": "Opções",
|
||||
"actions": "Ações",
|
||||
"edit": "Editar",
|
||||
"admin": "Admin",
|
||||
"places": "Lugares",
|
||||
"hide": "Ocultar",
|
||||
"search": "Buscar",
|
||||
"info": "Info",
|
||||
"users": "Usuários",
|
||||
"share": "Compartilhar",
|
||||
"name": "Nome",
|
||||
"associate": "Associar",
|
||||
"logout": "Sair",
|
||||
"remove_admin": "Remover admin",
|
||||
"activate": "Ativar",
|
||||
"save": "Salvar",
|
||||
"preview": "Pré-visualizar",
|
||||
"edit_event": "Editar evento",
|
||||
"add": "Adicionar",
|
||||
"copy": "Copiar",
|
||||
"ok": "Ok",
|
||||
"cancel": "Cancelar",
|
||||
"me": "Você",
|
||||
"password_updated": "Senha alterada.",
|
||||
"resources": "Recursos",
|
||||
"activate_user": "Confirmado",
|
||||
"copy_link": "Copiar link",
|
||||
"send_via_mail": "Enviar e-mail",
|
||||
"add_to_calendar": "Adicionar ao calendário",
|
||||
"copied": "Copiado",
|
||||
"recover_password": "Recuperar senha",
|
||||
"new_password": "Nova senha",
|
||||
"new_user": "Novo usuário",
|
||||
"deactivate": "Desligar",
|
||||
"related": "Relacionado",
|
||||
"enable": "Habilitar",
|
||||
"disable": "Desabilitar",
|
||||
"federation": "Federação",
|
||||
"set_password": "Definir senha",
|
||||
"instances": "Instâncias",
|
||||
"follow": "Seguir",
|
||||
"moderation": "Moderação",
|
||||
"user": "Usuário",
|
||||
"authorize": "Autorizar",
|
||||
"title": "Título",
|
||||
"filter": "Filtro",
|
||||
"event": "Evento",
|
||||
"pause": "Pausa",
|
||||
"start": "Início",
|
||||
"feed": "Feed RSS",
|
||||
"skip": "Pular",
|
||||
"delete": "Remover",
|
||||
"fediverse": "Fediverso",
|
||||
"announcements": "Anúncios",
|
||||
"place": "Local",
|
||||
"url": "URL",
|
||||
"logout_ok": "Deslogado",
|
||||
"n_resources": "nenhum recurso|um recurso|{n} recursos",
|
||||
"embed": "Incorporar",
|
||||
"embed_title": "Incorpore este evento em sua página",
|
||||
"embed_help": "Copie o seguinte código em sua página e o evento será exibido desta maneira",
|
||||
"displayname": "Nome de exibição",
|
||||
"feed_url_copied": "Abra a URL copiada do feed em seu leitor de RSS",
|
||||
"follow_me_title": "Siga as atualizações pelo Fediverso",
|
||||
"tags": "Marcadores",
|
||||
"theme": "Tema",
|
||||
"reset": "Reiniciar",
|
||||
"import": "Importar",
|
||||
"collections": "Coleções",
|
||||
"max_events": "N. máximo de eventos",
|
||||
"label": "Etiqueta",
|
||||
"close": "Fechar",
|
||||
"plugins": "Plugins",
|
||||
"help_translate": "Ajude a traduzir",
|
||||
"show_map": "Mostrar mapa",
|
||||
"calendar": "Calendário",
|
||||
"home": "Início",
|
||||
"about": "Sobre",
|
||||
"content": "Conteúdo",
|
||||
"admin_actions": "Ações de admin",
|
||||
"recurring_event_actions": "Ações de eventos recorrentes"
|
||||
},
|
||||
"admin": {
|
||||
"user_block_confirm": "Você está certo que quer bloquear o usuário {user}?",
|
||||
"filter_instances": "Filtrar instâncias",
|
||||
"user_add_help": "Um e-mail com instruções para confirmar a inscrição e escolher uma senha será enviada ao novo usuário",
|
||||
"show_resource": "Mostrar recurso",
|
||||
"block_user": "Bloquear usuário",
|
||||
"filter_users": "Filtrar usuários",
|
||||
"hide_resource": "Ocultar recurso",
|
||||
"delete_resource": "Remover recurso",
|
||||
"delete_resource_confirm": "Você está certo que quer remover este recurso?",
|
||||
"show_smtp_setup": "Configurações de e-mail",
|
||||
"resources": "Recursos",
|
||||
"delete_announcement_confirm": "Você está certo que quer remover o anúncio?",
|
||||
"smtp_hostname": "Hostname SMTP",
|
||||
"collections_description": "Coleções são agrupamentos de eventos por marcadores e locais. Eles serão exibidos na página principal",
|
||||
"new_collection": "Nova coleção",
|
||||
"disable_admin_user_confirm": "Você está certo que quer remover permissões de administração de {user}?",
|
||||
"enable_admin_user_confirm": "Você está certo que quer adicionar permissões de administrador para {user}?",
|
||||
"event_remove_ok": "Evento removido",
|
||||
"smtp_description": "<ul><li>Administrador deve receber um e-mail quando um evento anônimo for adicionado (se habilitado).</li><li>Administrador deve receber um e-mail de requisição de registro (se habilitado).</li><li>Usuário deve receber um e-mail de solicitação de registro.</li><li>Usuário deve receber um e-mail de confirmação de registro.</li><li>Usuário deve recever um e-mail de confirmação quando registrado diretamente por um administrador.</li><li>Usuários devem receber um e-mail para recuperar a senha quando eles esquecerem ela</li></ul>",
|
||||
"allow_registration_description": "Permitir registro aberto de usuários?",
|
||||
"block": "Bloquear",
|
||||
"unblock": "Desbloquear",
|
||||
"instance_name": "Nome da instância",
|
||||
"favicon": "Logo",
|
||||
"instance_block_confirm": "Você está certo que quer bloquear a instância {instance}?",
|
||||
"enable_federation": "Habilitar federação",
|
||||
"enable_federation_help": "Será possível seguir está instância a partir do Fediverso",
|
||||
"add_instance": "Adicionar instância",
|
||||
"hide_boost_bookmark_help": "Ocultar os pequenos ícones mostrando o número de impulsos e favoritos vindos do Fediverso",
|
||||
"announcement_remove_ok": "Anúncio removido",
|
||||
"instance_timezone_description": "Gancio é desenvolvido para coletar os eventos de um local específico, como uma cidade. Todos os eventos nesse local serão exibidos no fuso-horário escolhido para ele.",
|
||||
"instance_locale_description": "Idioma de preferência para as páginas. Algumas vezes as mensagens precisam ser exibidas em um mesmo idioma para todos (por exemplo quando publicando através do ActivityPub ou quando enviar alguns e-mails). Nestes casos o idioma seleciona abaixo será o utilizado.",
|
||||
"title_description": "É utilizado no título da página, no assunto do e-mail para exportar feeds RSS e ICS.",
|
||||
"instance_name_help": "Conta ActivityPub para seguir",
|
||||
"enable_trusted_instances": "Habilitar instâncias amigáveis",
|
||||
"trusted_instances_help": "A lista de instâncias amigáveis que serão exibidas no cabeçalho",
|
||||
"trusted_instances_label": "Etiqueta de navegação para instâncias amigas",
|
||||
"trusted_instances_label_default": "Instâncias amigas",
|
||||
"trusted_instances_label_help": "A etiqueta padrão é 'Instâncias amigas'",
|
||||
"add_trusted_instance": "Adicione uma instância amigável",
|
||||
"footer_links": "Links de rodapé",
|
||||
"delete_footer_link_confirm": "Está certo que quer remover este link?",
|
||||
"edit_place": "Editar local",
|
||||
"smtp_port": "Porta SMTP",
|
||||
"smtp_secure": "SMTP Seguro (TLS ou STARTTLS)",
|
||||
"smtp_use_sendmail": "Utilizar sendmail",
|
||||
"instance_place_help": "A etiqueta para exibir em outras instâncias",
|
||||
"delete_trusted_instance_confirm": "Você quer realmente remover este item do menu de instâncias amigas?",
|
||||
"sender_email": "E-mail do remetente",
|
||||
"widget": "Widget",
|
||||
"wrong_domain_warning": "A baseurl configurado em config.json <b>({baseurl})</b> é diferente da que você está visitando <b>({url})</b>",
|
||||
"edit_collection": "Editar Coleção",
|
||||
"enable_resources_help": "Permitir adicionar recursos para o evento a partir do Fediverso",
|
||||
"hide_boost_bookmark": "Ocultar impulsos/favoritos",
|
||||
"select_instance_timezone": "Fuso horário",
|
||||
"instance_locale": "Idioma padrão",
|
||||
"enable_resources": "Habilitar recursos",
|
||||
"delete_user": "Remover",
|
||||
"remove_admin": "Remover administrador",
|
||||
"place_description": "Se você colocou o local ou o endereço errado, você pode alterá-lo.<br/>Todos os eventos atuais e passados associados com esse local terão o endereço alterado.",
|
||||
"event_confirm_description": "Você pode confirmar eventos criados por usuários anônimos aqui",
|
||||
"disable_user_confirm": "Você está certo que quer desabilitar {user}?",
|
||||
"delete_user_confirm": "Você está certo que quer remover {user}?",
|
||||
"user_remove_ok": "Usuário removido",
|
||||
"user_create_ok": "Usuário criado",
|
||||
"allow_anon_event": "Permitir eventos anônimos (precisam ser confirmados)?",
|
||||
"allow_recurrent_event": "Permitir eventos recorrentes",
|
||||
"recurrent_event_visible": "Exibir eventos recorrentes por padrão",
|
||||
"user_blocked": "Usuário {user} bloqueado",
|
||||
"announcement_description": "Nesta seção você pode inserir anúncios que serão exibidos na página principal",
|
||||
"description_description": "Exibido no cabeçalho próximo ao título",
|
||||
"instance_place": "Local indicativo desta instância",
|
||||
"is_dark": "Tema escuro",
|
||||
"add_link": "Adicionar link",
|
||||
"new_announcement": "Novo anúncio",
|
||||
"federation": "Federação / ActivityPub",
|
||||
"smtp_test_success": "Um e-mail de teste foi enviado para {admin_email}, por favor verifique sua caixa de entrada",
|
||||
"smtp_test_button": "Enviar e-mail de teste",
|
||||
"allow_geolocation": "Permitir geolocalização de eventos",
|
||||
"config_plugin": "Configuração de plugin",
|
||||
"fallback_image": "Imagem alternativa",
|
||||
"header_image": "Imagem de cabeçalho",
|
||||
"hide_thumbs": "Ocultar miniaturas",
|
||||
"default_images_help": "Você precisa <a href='/admin?tab=theme'>recarregar</a> a página para ver as mudanças.",
|
||||
"domain": "Domínio",
|
||||
"default_images": "Imagens padrão",
|
||||
"known_users": "Usuários conhecidos",
|
||||
"created_at": "Criado em",
|
||||
"hide_calendar": "Ocultar calendário",
|
||||
"blocked": "Bloqueado"
|
||||
},
|
||||
"event": {
|
||||
"follow_me_description": "Uma das maneiras de se manter atualizado com os eventos publicados aqui em {title},\né seguir a conta <u>{account}</u> no Fediverso, por exemplo via Mastodon, e possivelmente adicionar recursos para um evento a partir de lá.<br/><br/>\nSe você nunca ouviu falar sobre Mastodon ou do Fediverso nós recomendamos ler <a href='https://www.savjee.be/videos/simply-explained/mastodon-and-fediverse-explained/'>este artigo</a>.<br/><br/>Entre com sua instância abaixo (e.g. mastodon.social)",
|
||||
"saved": "Evento salvo",
|
||||
"recurrent": "Recorrente",
|
||||
"ics": "ICS",
|
||||
"recurrent_1m_days": "Dia {days} de cada mês",
|
||||
"interact_with_me": "Siga-me",
|
||||
"media_description": "Você pode adicionar um flyer (opcional)",
|
||||
"same_day": "no mesmo dia",
|
||||
"added": "Evento adicionado",
|
||||
"what_description": "Título",
|
||||
"description_description": "Descrição",
|
||||
"tag_description": "Marcador",
|
||||
"show_recurrent": "eventos recorrentes",
|
||||
"anon": "Anônimo",
|
||||
"anon_description": "Você pode adicionar um evento sem se registrar ou autenticar-se, mas você precisa esperar alguém para ler,\nconfirmar que é um evento adequado. Não será possível modifiá-lo.<br/><br/>\nVocê pode ao invés <a href='/login'>autenticar-se</a> ou <a href='/register'>registrar-se</a>. De qualquer modo, vá em frente e consiga uma resposta o mais rápido possível. ",
|
||||
"added_anon": "Evento adicionado, mas ainda precisa ser confirmado.",
|
||||
"updated": "Evento atualizado",
|
||||
"where_description": "Onde está o evento? Se não está visível você pode criá-lo.",
|
||||
"confirmed": "Evento confirmado",
|
||||
"not_found": "Não foi possível encontrar o evento",
|
||||
"remove_confirmation": "Você está certo que quer remover este evento?",
|
||||
"edit_recurrent": "Editar evento recorrente:",
|
||||
"only_future": "apenas eventos futuros",
|
||||
"recurrent_description": "Escolha a frequência e selecione os dias",
|
||||
"multidate_description": "É um festival? Escolha quando ele começa e quando termina",
|
||||
"multidate": "Mais dias",
|
||||
"normal": "Normal",
|
||||
"show_past": "também eventos anteriores",
|
||||
"normal_description": "Escolha o dia.",
|
||||
"recurrent_1w_days": "A cada {days}",
|
||||
"each_week": "Toda semana",
|
||||
"each_2w": "A cada duas semanas",
|
||||
"each_month": "Todo mês",
|
||||
"recurrent_2w_days": "{days} a cada dois",
|
||||
"recurrent_2m_days": "Dia {days} a cada dois meses",
|
||||
"recurrent_1m_ordinal": "{n} {days} de cada mês",
|
||||
"recurrent_2m_ordinal": "{n} {days} a cada dois meses",
|
||||
"due": "até",
|
||||
"from": "De",
|
||||
"image_too_big": "A imagem não pode ser maior que 4MB",
|
||||
"interact_with_me_at": "Interaja comigo no Fediverso atráves de",
|
||||
"remove_recurrent_confirmation": "Você está certo que quer remover este evento recorrente?\nEventos passados serão mantidos, mas nenhum evento seguinte será criado.",
|
||||
"import_URL": "Importar a partir de URL",
|
||||
"import_ICS": "Importar a partir de ICS",
|
||||
"import_description": "Você pode importar eventos de outras plataformas e outras instâncias através de formatos padrão (ics e h-event)",
|
||||
"alt_text_description": "Descrição para pessoas com deficiências visuais",
|
||||
"choose_focal_point": "Escolha o ponto focal",
|
||||
"remove_media_confirmation": "Você confirma a remoção da imagem?",
|
||||
"download_flyer": "Baixar flyer",
|
||||
"address_description": "Qual é o endereço?",
|
||||
"address_description_osm": "Qual é o endereço? (contribuidores do <a href='http://osm.org/copyright'>OpenStreetMap</a>)"
|
||||
},
|
||||
"confirm": {
|
||||
"not_valid": "Algo deu errado.",
|
||||
"title": "Confirmação de usuário",
|
||||
"valid": "Sua conta está confirmada, você pode agora <a href=\"/login\">autenticar-se</a>"
|
||||
},
|
||||
"export": {
|
||||
"intro": "Diferente de plataformas não-sociais que fazem tudo para manter os usuários e os dados sobre eles, nós acreditamos que a informação, como pessoas, deve ser livre. Para isso você se atualizar sobre os eventos de seu interesse, sem necessariamente entrar nesta página.",
|
||||
"insert_your_address": "Informe seu endereço de e-mail",
|
||||
"feed_description": "Para seguir as atualizações de um computador ou smartphone sem que você precise acessar essa página periodicamente, utilize um feeds RSS. </p>\n\n<p> Com feeds RSS você pode utilizar um app especial para receber atualizações de páginas que te interessam. É uma boa maneira de seguir muitas páginas rapidamente, sem a necessidade de criar contas de usuários ou outras complicações. </p>\n\n<li> Se você possui um Android, recomendamos <a href=\"https://f-droid.org/en/packages/net.frju.flym/\">Flym</a> ou Feeder </li>\n<li> Para iPhone / iPad você pode utilizar <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\"> Feed4U </a> </li>\n<li> Para desktop / laptop nós recomendamos Feedbro, que pode ser instalado no <a href=\"https://addons.mozilla.org/en-GB/firefox/addon/feedbroreader/\"> Firefox </a> ou <a href=\"https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa\"> Chrome </a>. </li>\n<br/>\nAdicionado este link ao seu leitor de RSS irá mantê-lo atualizado.",
|
||||
"ical_description": "Computadores e smartphones normalmente possuem aplicações de calendário capazes de importar um calendário remoto.",
|
||||
"list_description": "Se você tem uma página e quer exibir uma lista de eventos, use o seguinte código",
|
||||
"email_description": "Você pode obter os eventos que te interessam através de e-mail."
|
||||
},
|
||||
"oauth": {
|
||||
"authorization_request": "A aplicação <code>{app}</code> solicita autorização em <code>{instance_name}</code>:",
|
||||
"scopes": {
|
||||
"event:write": "Adicionar e editar os seus eventos"
|
||||
},
|
||||
"redirected_to": "Depois da confirmação, você será redirecionado para <code>{url}</code>"
|
||||
},
|
||||
"ordinal": {
|
||||
"2": "segundo",
|
||||
"1": "primeiro",
|
||||
"5": "quinto",
|
||||
"-1": "último",
|
||||
"3": "terceiro",
|
||||
"4": "quarto"
|
||||
},
|
||||
"settings": {
|
||||
"remove_account": "Ao pressionar este botão sua conta de usuário será removida. Eventos que você publicou serão mantidos.",
|
||||
"remove_account_confirm": "Você está prestes a remover sua conta de usuário permanentemente",
|
||||
"update_confirm": "Você quer salvar sua modificação?",
|
||||
"change_password": "Alterar sua senha",
|
||||
"password_updated": "Senha alterada.",
|
||||
"danger_section": "Seção perigosa"
|
||||
},
|
||||
"error": {
|
||||
"email_taken": "Este e-mail já está em uso.",
|
||||
"nick_taken": "Este nome de usuário já está em uso."
|
||||
},
|
||||
"setup": {
|
||||
"https_warning": "Você está acessando por HTTP, lembre-se de alterar o valor de baseurl em config.json se você mudar para HTTPS!",
|
||||
"completed": "Configuração completa",
|
||||
"start": "Iniciar",
|
||||
"completed_description": "<p>Você pode agora autenticar-se com o seguinte usuário:<br/><br/>Usuário: <b>{email}</b><br/>Senha: <b>{password}<b/></p>",
|
||||
"copy_password_dialog": "Sim, você precisa copiar a senha!"
|
||||
},
|
||||
"validators": {
|
||||
"email": "Insira um e-mail válido",
|
||||
"required": "{fieldName} é obrigatório"
|
||||
},
|
||||
"about": "\n <p><a href='https://gancio.org'>Gancio</a> é uma agenda compartilhada para comunidades locais.</p>\n ",
|
||||
"login": {
|
||||
"not_registered": "Não registrado?",
|
||||
"forgot_password": "Esqueceu sua senha?",
|
||||
"insert_email": "Informe seu endereço de e-mail",
|
||||
"ok": "Autenticado",
|
||||
"description": "Ao autenticar-se você pode publicar novos eventos.",
|
||||
"check_email": "Verifique sua caixa de entrada e de spam.",
|
||||
"error": "Autenticação não realizada. Verifique suas informações de autenticação."
|
||||
},
|
||||
"recover": {
|
||||
"not_valid_code": "Algo não funcionou corretamente."
|
||||
},
|
||||
"register": {
|
||||
"error": "Erro: ",
|
||||
"complete": "Registro precisa ser confirmado.",
|
||||
"first_user": "Administrador criado",
|
||||
"description": "Movimentos sociais devem se organizar e se auto financiar.<br/>\n<br/>Antes de poder publicar, <strong> a conta precisa ser aprovada</strong>, considere que <strong> atrás deste site você vai encontrar pessoas reais</strong>, então escreva duas linhas para nos informar que eventos você gostaria de publicar."
|
||||
},
|
||||
"auth": {
|
||||
"fail": "Autenticação não efetuada. Você está certo que sua senha está correta?",
|
||||
"not_confirmed": "Não confirmado ainda…"
|
||||
}
|
||||
}
|
||||
|
|
293
locales/zh.json
Normal file
293
locales/zh.json
Normal file
|
@ -0,0 +1,293 @@
|
|||
{
|
||||
"common": {
|
||||
"embed_help": "将以下代码复制并粘贴到你的网站,此事件将像这样显示",
|
||||
"follow": "关注",
|
||||
"add_event": "添加事件",
|
||||
"send": "发送",
|
||||
"where": "地点",
|
||||
"address": "地址",
|
||||
"when": "时间",
|
||||
"what": "事件",
|
||||
"media": "媒体",
|
||||
"login": "登录",
|
||||
"email": "电子邮箱",
|
||||
"register": "注册",
|
||||
"description": "描述",
|
||||
"hide": "隐藏",
|
||||
"search": "搜索",
|
||||
"confirm": "确认",
|
||||
"admin": "管理员",
|
||||
"users": "用户",
|
||||
"events": "事件",
|
||||
"actions": "操作",
|
||||
"deactivate": "取消",
|
||||
"remove_admin": "移除管理员",
|
||||
"activate": "激活",
|
||||
"save": "该村",
|
||||
"logout": "登出",
|
||||
"name": "名称",
|
||||
"associate": "合作者",
|
||||
"add": "添加",
|
||||
"recover_password": "重置密码",
|
||||
"enable": "启用",
|
||||
"me": "你",
|
||||
"ok": "完成",
|
||||
"resources": "资源",
|
||||
"n_resources": "无资源|1 个资源|{n} 个资源",
|
||||
"displayname": "显示名称",
|
||||
"copy_link": "复制链接",
|
||||
"send_via_mail": "发送电子邮件",
|
||||
"embed": "嵌入式页面",
|
||||
"feed_url_copied": "在你的 RSS 阅读器中打开复制的链接",
|
||||
"follow_me_title": "在 Fediverse 网络中关注更新",
|
||||
"feed": "RSS 源",
|
||||
"moderation": "中等",
|
||||
"authorize": "认证",
|
||||
"title": "标题",
|
||||
"filter": "筛选",
|
||||
"pause": "暂停",
|
||||
"start": "开始",
|
||||
"announcements": "公告",
|
||||
"url": "URL",
|
||||
"place": "地点",
|
||||
"theme": "主题",
|
||||
"label": "标签",
|
||||
"collections": "收藏",
|
||||
"max_events": "最大事件数",
|
||||
"next": "下一个",
|
||||
"export": "导出",
|
||||
"remove": "移除",
|
||||
"settings": "选项",
|
||||
"logout_ok": "已登出",
|
||||
"new_password": "新密码",
|
||||
"new_user": "新用户",
|
||||
"places": "地点",
|
||||
"edit": "编辑",
|
||||
"cancel": "取消",
|
||||
"password": "密码",
|
||||
"info": "信息",
|
||||
"preview": "预览",
|
||||
"share": "分享",
|
||||
"edit_event": "编辑事件",
|
||||
"copy": "复制",
|
||||
"related": "相关",
|
||||
"set_password": "设置密码",
|
||||
"instances": "实例",
|
||||
"activate_user": "已确认",
|
||||
"federation": "联盟",
|
||||
"add_to_calendar": "添加到日历",
|
||||
"copied": "已复制",
|
||||
"embed_title": "在你的网页上嵌入此事件",
|
||||
"user": "用户",
|
||||
"event": "事件",
|
||||
"fediverse": "Fediverse 网络",
|
||||
"skip": "跳过",
|
||||
"delete": "移除",
|
||||
"import": "导入",
|
||||
"tags": "标签",
|
||||
"close": "关闭",
|
||||
"disable": "禁用",
|
||||
"password_updated": "密码已修改。",
|
||||
"reset": "重置"
|
||||
},
|
||||
"export": {
|
||||
"list_description": "如果你有一个网站并希望展示一个事件列表,使用以下代码",
|
||||
"email_description": "你可以通过发给你的电子邮件了解你感兴趣的事件。",
|
||||
"insert_your_address": "输入你的电子邮箱地址",
|
||||
"ical_description": "电脑和智能手机通常预装了能够导入远程日历的日历应用。",
|
||||
"intro": "与那些竭尽全力保留用户和数据的非社交平台不同,我们认为信息和人一样,都必须是自由的。因此,即使不通过此网站,你仍可随时了解你想了解的事件之最新情况。",
|
||||
"feed_description": "要想从电脑或智能手机上关注更新,而无需定期打开本网站,请使用RSS订阅。</p>\n\n<p>通过RSS订阅,您可以使用一个特殊的应用程序来接收你感兴趣的网站的更新。这是一个快速关注许多网站的好方法,不需要创建账户或做其他事。</p>\n\n<li> 如果您使用 Android,我们推荐 <a href=\"https://f-droid.org/en/packages/net.frju.flym/\">Flym</a> 或 Feeder。</li>\n<li> 对于iPhone/iPad,您可以使用 <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\">Feed4U</a> </li>\n<li> 对于台式机/笔记本电脑,我们推荐可在 <a href=\"https://addons.mozilla.org/en-GB/firefox/addon/feedbroreader/\">Firefox</a> 或 <a href=\"https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa\">Chrome</a> 上安装的 Feedbro。</li>\n<br/>\n将此链接添加到你的RSS阅读器中,以获得最新信息。"
|
||||
},
|
||||
"register": {
|
||||
"description": "社会运动应该组织起来,并自筹资金<br/>\n<br/>在你能发布内容之前,<strong>你的账户必须审核通过</strong>,考虑到<strong>你可以通过此网站发现现实中的人</strong>,请写一些东西告诉我们你希望发布什么。",
|
||||
"error": "错误: ",
|
||||
"complete": "注册必须经过确认。",
|
||||
"first_user": "管理员已创建"
|
||||
},
|
||||
"event": {
|
||||
"anon_description": "即使不登录或注册,你也可以创建事件,但必须等待一些人看到它,\n并确认这是一个合适的事件。此外你也不能够修改它。<br/><br/>\n你也可以 <a href='/login'>登录</a> 或 <a href='/register'>注册</a>。或者继续浏览以得到答案。 ",
|
||||
"anon": "匿名",
|
||||
"same_day": "在同一天",
|
||||
"what_description": "标题",
|
||||
"description_description": "描述",
|
||||
"added": "事件已添加",
|
||||
"saved": "事件已保存",
|
||||
"added_anon": "事件已添加,等待确认。",
|
||||
"updated": "事件已更新",
|
||||
"where_description": "事件的地点在哪里?如果不存在,你可以创建一个。",
|
||||
"confirmed": "事件已确认",
|
||||
"not_found": "找不到事件",
|
||||
"recurrent": "日常事件",
|
||||
"edit_recurrent": "编辑日常事件:",
|
||||
"show_recurrent": "日常事件",
|
||||
"show_past": "以及过往的事件",
|
||||
"multidate_description": "这是一个节日吗?选择它开始和结束的时间",
|
||||
"multidate": "更多日期",
|
||||
"normal_description": "选择日期。",
|
||||
"recurrent_2w_days": "每 {days} 天一次",
|
||||
"each_week": "每周",
|
||||
"each_2w": "隔周一次",
|
||||
"due": "直到",
|
||||
"from": "来自",
|
||||
"image_too_big": "图片不能大于 4MB",
|
||||
"interact_with_me_at": "在 Fediverse 网络上与我互动",
|
||||
"interact_with_me": "关注我",
|
||||
"remove_recurrent_confirmation": "你确定要移除这个日常事件吗?\n过去的事件仍将被维护,但不会再添加新事件。",
|
||||
"import_URL": "从 URL 导入",
|
||||
"import_ICS": "从 ICS 导入",
|
||||
"ics": "ICS",
|
||||
"alt_text_description": "为视觉障碍者提供的说明",
|
||||
"choose_focal_point": "选择联络点",
|
||||
"download_flyer": "下载传单",
|
||||
"tag_description": "标签",
|
||||
"media_description": "你可以添加一份传单(可选)",
|
||||
"recurrent_description": "选择频率和日期",
|
||||
"only_future": "仅限即将到来的事件",
|
||||
"normal": "普通",
|
||||
"recurrent_1w_days": "每 {days} 天",
|
||||
"recurrent_1m_days": "|每月的第 {days} 天|每月的第 {days} 天",
|
||||
"recurrent_1m_ordinal": "每月的第 {n} 个 {days}",
|
||||
"each_month": "每月",
|
||||
"follow_me_description": "一种对这里发布的 {title} 事件保持关注的方法,\n是在 Fediverse 网络,比如 Mastodon,上关注 <u>{account}</u>,亦有可能通过此方式给此事件添加资源。<br/><br/>\n如果你没听说过 Mastodon 和 Fediverse,我们建议你阅读 <a href='https://www.savjee.be/videos/simply-explained/mastodon-and-fediverse-explained/'>这篇文章</a>。<br/><br/>在下方输入你的实例名称(例如:mastodon.social)",
|
||||
"import_description": "你可以从其他平台和实例通过标准格式(ICS 和 hCalendar)导入事件",
|
||||
"remove_media_confirmation": "你确认要删除图片吗?",
|
||||
"remove_confirmation": "你确定要移除此事件吗?"
|
||||
},
|
||||
"login": {
|
||||
"check_email": "检查你的电子邮箱收件箱和垃圾邮件箱。",
|
||||
"not_registered": "还未注册?",
|
||||
"forgot_password": "忘记密码了?",
|
||||
"insert_email": "输入你的电子邮箱地址",
|
||||
"error": "无法登录,检查你的登录信息。",
|
||||
"ok": "已登录",
|
||||
"description": "登录以发布新事件。"
|
||||
},
|
||||
"recover": {
|
||||
"not_valid_code": "发生了一些错误。"
|
||||
},
|
||||
"admin": {
|
||||
"delete_user": "移除",
|
||||
"remove_admin": "移除管理员",
|
||||
"disable_user_confirm": "你确定禁用 {user} 吗?",
|
||||
"disable_admin_user_confirm": "你确定移除 {user} 的管理员权限吗?",
|
||||
"user_remove_ok": "用户已移除",
|
||||
"user_create_ok": "用户已创建",
|
||||
"event_remove_ok": "事件已移除",
|
||||
"allow_registration_description": "允许公众注册?",
|
||||
"allow_anon_event": "允许发布匿名事件(需要确认)?",
|
||||
"allow_recurrent_event": "允许日常事件",
|
||||
"federation": "联邦社交网络 / ActivityPub",
|
||||
"enable_federation": "启用联邦社交网络",
|
||||
"add_instance": "添加实例",
|
||||
"select_instance_timezone": "时区",
|
||||
"enable_resources": "启用资源",
|
||||
"hide_boost_bookmark": "隐藏助力/书签",
|
||||
"block": "屏蔽",
|
||||
"unblock": "解除屏蔽",
|
||||
"instance_name": "实例名称",
|
||||
"hide_resource": "隐藏资源",
|
||||
"delete_resource_confirm": "你确定要删除此资源吗?",
|
||||
"filter_instances": "筛选实例",
|
||||
"resources": "资源",
|
||||
"favicon": "图标",
|
||||
"user_block_confirm": "你确定要屏蔽用户 {user} 吗?",
|
||||
"delete_announcement_confirm": "你确定要移除这个公告吗?",
|
||||
"announcement_remove_ok": "公告已移除",
|
||||
"instance_locale": "默认语言",
|
||||
"title_description": "这将被用作页面的标题和电子邮件的主题,以导出 RSS 和 ICS 源。",
|
||||
"description_description": "在标题旁的页眉中显示",
|
||||
"instance_place_help": "在其他的实例中显示的标签",
|
||||
"delete_trusted_instance_confirm": "你确定要从友好实例菜单中删除此项目吗?",
|
||||
"edit_place": "编辑地点",
|
||||
"new_announcement": "新公告",
|
||||
"show_smtp_setup": "电子邮件设置",
|
||||
"smtp_port": "SMTP 端口",
|
||||
"smtp_secure": "SMTP 安全协议(TLS 或 STARTTLS)",
|
||||
"smtp_test_success": "一封测试邮件已发送至 {admin_mail},请检查你的收件箱",
|
||||
"sender_email": "发件人",
|
||||
"widget": "小组件",
|
||||
"wrong_domain_warning": "在 config.json 中设置的 baseurl <b>{baseurl}</b> 与你正在访问的 <b>{url}</b> 不同",
|
||||
"edit_collection": "编辑收藏",
|
||||
"event_confirm_description": "你可以在此确认匿名用户提交的事件",
|
||||
"recurrent_event_visible": "默认显示日常事件",
|
||||
"place_description": "如果你弄错了地点或地址,你可以修改。<br/>与这个地点相关的当前和过去的所有事件都会改变地址。",
|
||||
"delete_user_confirm": "你确定移除 {user} 吗?",
|
||||
"enable_admin_user_confirm": "你确定授予 {user} 管理员权限吗",
|
||||
"enable_federation_help": "这将允许从 Fediverse 上关注此实例",
|
||||
"enable_resources_help": "允许从 Fediverse 为此事件添加资源",
|
||||
"hide_boost_bookmark_help": "隐藏来自 Fediverse 上的助力和书签数量图标",
|
||||
"block_user": "屏蔽用户",
|
||||
"filter_users": "筛选用户",
|
||||
"user_add_help": "一封带有确认订阅和设置密码指引的邮件将被发送给新用户",
|
||||
"show_resource": "显示资源",
|
||||
"delete_resource": "删除资源",
|
||||
"user_blocked": "用户 {user} 已屏蔽",
|
||||
"instance_block_confirm": "你确定要屏蔽实例 {instance} 吗?",
|
||||
"announcement_description": "你可以在此段落插入显示于首页的公告",
|
||||
"instance_timezone_description": "Gancio 被设计用来收集特定区域,比如一座城市所发生的事件。这里的所有事件将以所选择的时区显示。",
|
||||
"instance_locale_description": "特定页面偏好的用户语言。有时信息必须以相同的语言对所有人显示(比如通过 ActivityPub 发布内容或发送一些电子邮件时)。在这种情况下将使用上面选择的语言。",
|
||||
"instance_place": "该实例的指示性位置",
|
||||
"trusted_instances_help": "友好实例的列表将被显示于页眉",
|
||||
"footer_links": "页脚链接",
|
||||
"smtp_description": "<ul><li>当匿名事件被添加时(如果启用),管理员应当收到邮件</li>管理员应当会受到注册请求邮件(如果启用)。<li></li><li>用户应当会受到注册请求邮件。</li><li>用户应当收到注册确认邮件。</li><li>当管理员直接订阅时,用户应当受到邮件。</li><li>用户忘记密码时应当收到密码重置邮件。</li></ul>",
|
||||
"smtp_use_sendmail": "使用 sendmail",
|
||||
"smtp_test_button": "发送测试邮件",
|
||||
"new_collection": "新建收藏",
|
||||
"collections_description": "收藏是按标签和地点分组的事件。它们将于主页上显示",
|
||||
"enable_trusted_instances": "启用友好实例",
|
||||
"add_trusted_instance": "添加一个友好实例",
|
||||
"add_link": "添加链接",
|
||||
"delete_footer_link_confirm": "确定移除此链接吗?",
|
||||
"instance_name_help": "要关注的 ActivityPub 账号",
|
||||
"is_dark": "暗色主题",
|
||||
"smtp_hostname": "SMTP 主机名"
|
||||
},
|
||||
"auth": {
|
||||
"fail": "无法登录。你确定密码正确吗?",
|
||||
"not_confirmed": "尚未确认……"
|
||||
},
|
||||
"settings": {
|
||||
"change_password": "修改密码",
|
||||
"password_updated": "密码已修改。",
|
||||
"remove_account_confirm": "你即将永久删除你的账号",
|
||||
"update_confirm": "你希望保存你的修改吗?",
|
||||
"remove_account": "按下下方的按钮后你的账号将被删除。你发布的事件不会删除。",
|
||||
"danger_section": "危险段落"
|
||||
},
|
||||
"error": {
|
||||
"email_taken": "此电子邮箱地址已被使用。",
|
||||
"nick_taken": "此昵称已被使用。"
|
||||
},
|
||||
"confirm": {
|
||||
"title": "用户确认",
|
||||
"not_valid": "出现了一些错误。",
|
||||
"valid": "你的账户已被确认,你现在可以 <a href=\"/login\">登录</a>"
|
||||
},
|
||||
"ordinal": {
|
||||
"4": "第四",
|
||||
"5": "第五",
|
||||
"2": "第二",
|
||||
"-1": "最后",
|
||||
"1": "第一",
|
||||
"3": "第三"
|
||||
},
|
||||
"validators": {
|
||||
"required": "{fieldName} 是必填项",
|
||||
"email": "输入有效的电子邮箱地址"
|
||||
},
|
||||
"oauth": {
|
||||
"authorization_request": "应用 <code>{app}</code> 申请在 <code>{instance_name}</code> 上获得以下权限:",
|
||||
"redirected_to": "在确认后你将被重定向到 <code>{url}</code>",
|
||||
"scopes": {
|
||||
"event:write": "添加与编辑你的事件"
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
"completed_description": "<p>你现在可以以以下用户登录<br/><br/>用户名:<b>{email}</b><br/>密码:<b>{password}<b/></p>",
|
||||
"copy_password_dialog": "没错,你必须复制密码!",
|
||||
"start": "开始",
|
||||
"https_warning": "你正在使用 HTTP 访问,如果你切换到 HTTPS,记得在 config.json 中修改 baseurl!",
|
||||
"completed": "安装完成"
|
||||
},
|
||||
"about": "\n <p><a href='https://gancio.org'>Gancio</a> 是为本地社区设计的的共享日程表。</p>\n "
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
const config = require('./server/config.js')
|
||||
const minifyTheme = require('minify-css-string').default
|
||||
const locales = require('./locales/index')
|
||||
|
||||
import { ca, de, en, es, eu, fr, gl, it, nb, pl, pt, sk, zhHans } from 'vuetify/lib/locale'
|
||||
|
||||
const isDev = (process.env.NODE_ENV !== 'production')
|
||||
module.exports = {
|
||||
|
@ -30,16 +33,19 @@ module.exports = {
|
|||
/*
|
||||
** Customize the progress-bar component
|
||||
*/
|
||||
loading: '~/components/Loading.vue',
|
||||
loading: {
|
||||
color: 'orangered',
|
||||
height: '3px'
|
||||
}, //'~/components/Loading.vue',
|
||||
/*
|
||||
** Plugins to load before mounting the App
|
||||
*/
|
||||
plugins: [
|
||||
'@/plugins/i18n.js',
|
||||
'@/plugins/filters', // text filters, datetime filters, generic transformation helpers etc.
|
||||
'@/plugins/axios', // axios baseurl configuration
|
||||
'@/plugins/validators', // inject validators
|
||||
'@/plugins/api', // api helpers
|
||||
'@/plugins/i18n',
|
||||
{ src: '@/plugins/v-calendar', ssr: false } // v-calendar
|
||||
],
|
||||
|
||||
|
@ -48,6 +54,7 @@ module.exports = {
|
|||
*/
|
||||
modules: [
|
||||
// Doc: https://axios.nuxtjs.org/usage
|
||||
'@nuxtjs/i18n',
|
||||
'@nuxtjs/axios',
|
||||
'@nuxtjs/auth',
|
||||
'@nuxtjs/sitemap'
|
||||
|
@ -76,6 +83,24 @@ module.exports = {
|
|||
}
|
||||
}
|
||||
},
|
||||
i18n: {
|
||||
locales: Object.keys(locales).map(key => ({
|
||||
code: key,
|
||||
name: locales[key],
|
||||
file: `${key}.json`,
|
||||
iso: key
|
||||
})),
|
||||
vueI18n: {
|
||||
fallbackLocale: 'en',
|
||||
silentTranslationWarn: true
|
||||
},
|
||||
langDir: 'locales',
|
||||
lazy: true,
|
||||
strategy: 'no_prefix',
|
||||
baseUrl: config.baseurl,
|
||||
skipSettingLocaleOnNavigate: true,
|
||||
skipNuxtState: true
|
||||
},
|
||||
|
||||
serverMiddleware: ['server/routes'],
|
||||
|
||||
|
@ -87,6 +112,8 @@ module.exports = {
|
|||
prefix: '/api'
|
||||
},
|
||||
auth: {
|
||||
rewriteRedirects: true,
|
||||
fullPathRedirect: true,
|
||||
// localStorage: false, // https://github.com/nuxt-community/auth-module/issues/425
|
||||
cookie: {
|
||||
prefix: 'auth.',
|
||||
|
@ -105,7 +132,7 @@ module.exports = {
|
|||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
},
|
||||
logout: false,
|
||||
user: { url: '/user', method: 'get', propertyName: false }
|
||||
user: { url: '/user', method: 'get', propertyName: false, autoFetch: false }
|
||||
},
|
||||
tokenRequired: true,
|
||||
tokenType: 'Bearer'
|
||||
|
@ -114,6 +141,7 @@ module.exports = {
|
|||
},
|
||||
buildModules: ['@nuxtjs/vuetify'],
|
||||
vuetify: {
|
||||
lang: { locales: { ca, de, en, es, eu, fr, gl, it, nb, pl, pt, sk, zhHans } },
|
||||
treeShake: true,
|
||||
theme: {
|
||||
options: {
|
||||
|
|
54
package.json
54
package.json
|
@ -1,15 +1,15 @@
|
|||
{
|
||||
"name": "gancio",
|
||||
"version": "1.5.6",
|
||||
"version": "1.6.0",
|
||||
"description": "A shared agenda for local communities",
|
||||
"author": "lesion",
|
||||
"scripts": {
|
||||
"build": "nuxt build --modern",
|
||||
"start:inspect": "NODE_ENV=production node --inspect node_modules/.bin/nuxt start --modern",
|
||||
"dev": "nuxt dev",
|
||||
"test-sqlite": "export NODE_ENV=test; export DB=sqlite; jest --bail=1",
|
||||
"test-mariadb": "export NODE_ENV=test; export DB=mariadb; jest --bail=1",
|
||||
"test-postgresql": "export NODE_ENV=test; export DB=postgresql; jest --bail=1",
|
||||
"test-sqlite": "export NODE_ENV=test; export DB=sqlite; jest --testEnvironment=jest-environment-node --bail=1",
|
||||
"test-mariadb": "export NODE_ENV=test; export DB=mariadb; jest --testEnvironment=jest-environment-node --bail=1",
|
||||
"test-postgresql": "export NODE_ENV=test; export DB=postgresql; jest --testEnvironment=jest-environment-node --bail=1",
|
||||
"start": "nuxt start --modern",
|
||||
"doc": "cd docs && bundle exec jekyll b",
|
||||
"doc:dev": "cd docs && bundle exec jekyll s --drafts",
|
||||
|
@ -33,29 +33,31 @@
|
|||
"node": ">=14 <=16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/js": "^7.0.96",
|
||||
"@mdi/js": "^7.1.96",
|
||||
"@nuxtjs/auth": "^4.9.1",
|
||||
"@nuxtjs/axios": "^5.13.5",
|
||||
"@nuxtjs/i18n": "^7.3.0",
|
||||
"@nuxtjs/sitemap": "^2.4.0",
|
||||
"accept-language": "^3.0.18",
|
||||
"axios": "^0.27.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.20.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cookie-session": "^2.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"dayjs": "^1.11.5",
|
||||
"dompurify": "^2.3.10",
|
||||
"dayjs": "^1.11.7",
|
||||
"dompurify": "^2.4.1",
|
||||
"email-templates": "^10.0.1",
|
||||
"express": "^4.18.1",
|
||||
"express-oauth-server": "lesion/express-oauth-server#master",
|
||||
"http-signature": "^1.3.6",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"ical.js": "^1.5.0",
|
||||
"ics": "^2.40.0",
|
||||
"jsdom": "^20.0.0",
|
||||
"jsdom": "^20.0.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"linkify-html": "^4.0.0",
|
||||
"linkifyjs": "4.0.0",
|
||||
"leaflet": "^1.9.2",
|
||||
"linkify-html": "^4.0.2",
|
||||
"linkifyjs": "4.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"mariadb": "^3.0.1",
|
||||
"microformat-node": "^2.0.1",
|
||||
|
@ -64,39 +66,51 @@
|
|||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^2.3.3",
|
||||
"nuxt-edge": "2.16.0-27720022.54e852f",
|
||||
"oauth2orize": "^1.11.1",
|
||||
"passport": "^0.6.0",
|
||||
"passport-anonymous": "^1.0.1",
|
||||
"passport-custom": "^1.1.1",
|
||||
"passport-http": "^0.3.0",
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"passport-oauth2-client-password": "^0.1.2",
|
||||
"passport-oauth2-client-public": "^0.0.1",
|
||||
"pg": "^8.8.0",
|
||||
"sequelize": "^6.23.0",
|
||||
"sequelize": "^6.27.0",
|
||||
"sequelize-slugify": "^1.6.2",
|
||||
"sharp": "^0.27.2",
|
||||
"sqlite3": "^5.0.11",
|
||||
"sqlite3": "^5.1.4",
|
||||
"telegraf": "^4.9.1",
|
||||
"tiptap": "^1.32.0",
|
||||
"tiptap-extensions": "^1.35.0",
|
||||
"umzug": "^2.3.0",
|
||||
"v-calendar": "^2.4.1",
|
||||
"vue-i18n": "^8.26.7",
|
||||
"vuetify": "2.6.10",
|
||||
"vue2-leaflet": "^2.7.1",
|
||||
"vuetify": "2.6.13",
|
||||
"winston": "^3.8.2",
|
||||
"winston-daily-rotate-file": "^4.7.1",
|
||||
"yargs": "^17.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxtjs/vuetify": "^1.12.3",
|
||||
"jest": "^29.0.3",
|
||||
"prettier": "^2.7.1",
|
||||
"jest": "^29.3.1",
|
||||
"jest-environment-node": "^29.3.1",
|
||||
"prettier": "^2.8.1",
|
||||
"pug": "^3.0.2",
|
||||
"pug-plain-loader": "^1.1.0",
|
||||
"sass": "^1.53.0",
|
||||
"sass": "^1.56.2",
|
||||
"sequelize-cli": "^6.3.0",
|
||||
"supertest": "^6.2.4",
|
||||
"supertest": "^6.3.3",
|
||||
"webpack": "4",
|
||||
"webpack-cli": "^4.10.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"nth-check": "^2.0.1",
|
||||
"express-oauth-server/**/lodash": "^4.17.21",
|
||||
"glob-parent": "^5.1.2",
|
||||
"moment": "^2.29.2"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "jsdom"
|
||||
},
|
||||
"bin": {
|
||||
"gancio": "server/cli.js"
|
||||
},
|
||||
|
|
|
@ -6,51 +6,68 @@ v-container.container.pa-0.pa-md-3
|
|||
v-tabs(v-model='selectedTab' show-arrows :next-icon='mdiChevronRight' :prev-icon='mdiChevronLeft')
|
||||
|
||||
//- SETTINGS
|
||||
v-tab {{$t('common.settings')}}
|
||||
v-tab-item
|
||||
v-tab(href='#settings') {{$t('common.settings')}}
|
||||
v-tab-item(value='settings')
|
||||
Settings
|
||||
|
||||
//- THEME
|
||||
v-tab {{$t('common.theme')}}
|
||||
v-tab-item
|
||||
v-tab(href='#theme') {{$t('common.theme')}}
|
||||
v-tab-item(value='theme')
|
||||
Theme
|
||||
|
||||
//- USERS
|
||||
v-tab
|
||||
v-tab(href='#users')
|
||||
v-badge(:value='!!unconfirmedUsers.length' :content='unconfirmedUsers.length') {{$t('common.users')}}
|
||||
v-tab-item
|
||||
v-tab-item(value='users')
|
||||
Users(:users='users' @update='updateUsers')
|
||||
|
||||
//- PLACES
|
||||
v-tab {{$t('common.places')}}
|
||||
v-tab-item
|
||||
v-tab(href='#places') {{$t('common.places')}}
|
||||
v-tab-item(value='places')
|
||||
Places
|
||||
|
||||
//- TAGS
|
||||
v-tab(href='#tags') {{$t('common.tags')}}
|
||||
v-tab-item(value='tags')
|
||||
Tags
|
||||
|
||||
//- GEOCODING / MAPS
|
||||
v-tab(href='#geolocation' v-if='settings.allow_geolocation') {{$t('admin.geolocation')}}
|
||||
v-tab-item(value='geolocation')
|
||||
client-only(placeholder='Loading...')
|
||||
Geolocation
|
||||
|
||||
|
||||
//- Collections
|
||||
v-tab {{$t('common.collections')}}
|
||||
v-tab-item
|
||||
v-tab(href='#collections') {{$t('common.collections')}}
|
||||
v-tab-item(value='collections')
|
||||
Collections
|
||||
|
||||
//- EVENTS
|
||||
v-tab
|
||||
v-tab(href='#unconfirmed_events')
|
||||
v-badge(:value='!!unconfirmedEvents.length' :content='unconfirmedEvents.length') {{$t('common.events')}}
|
||||
v-tab-item
|
||||
v-tab-item(value='unconfirmed_events')
|
||||
Events(:unconfirmedEvents='unconfirmedEvents'
|
||||
@confirmed='id => { unconfirmedEvents = unconfirmedEvents.filter(e => e.id !== id)}')
|
||||
|
||||
//- ANNOUNCEMENTS
|
||||
v-tab {{$t('common.announcements')}}
|
||||
v-tab-item
|
||||
v-tab(href='#announcements') {{$t('common.announcements')}}
|
||||
v-tab-item(value='announcements')
|
||||
Announcement
|
||||
|
||||
//- PLUGINS
|
||||
v-tab(href='#plugins') {{$t('common.plugins')}}
|
||||
v-tab-item(value='plugins')
|
||||
Plugin
|
||||
|
||||
//- FEDERATION
|
||||
v-tab {{$t('common.federation')}}
|
||||
v-tab-item
|
||||
v-tab(href='#federation') {{$t('common.federation')}}
|
||||
v-tab-item(value='federation')
|
||||
Federation
|
||||
|
||||
//- MODERATION
|
||||
v-tab(v-if='settings.enable_federation') {{$t('common.moderation')}}
|
||||
v-tab-item
|
||||
v-tab(v-if='settings.enable_federation' href='#moderation') {{$t('common.moderation')}}
|
||||
v-tab-item(value='moderation')
|
||||
Moderation
|
||||
</template>
|
||||
<script>
|
||||
|
@ -65,9 +82,12 @@ export default {
|
|||
Users: () => import(/* webpackChunkName: "admin" */'../components/admin/Users'),
|
||||
Events: () => import(/* webpackChunkName: "admin" */'../components/admin/Events'),
|
||||
Places: () => import(/* webpackChunkName: "admin" */'../components/admin/Places'),
|
||||
Tags: () => import(/* webpackChunkName: "admin" */'../components/admin/Tags'),
|
||||
Collections: () => import(/* webpackChunkName: "admin" */'../components/admin/Collections'),
|
||||
[process.client && 'Geolocation']: () => import(/* webpackChunkName: "admin" */'../components/admin/Geolocation.vue'),
|
||||
Federation: () => import(/* webpackChunkName: "admin" */'../components/admin/Federation.vue'),
|
||||
Moderation: () => import(/* webpackChunkName: "admin" */'../components/admin/Moderation.vue'),
|
||||
Plugin: () => import(/* webpackChunkName: "admin" */'../components/admin/Plugin.vue'),
|
||||
Announcement: () => import(/* webpackChunkName: "admin" */'../components/admin/Announcement.vue'),
|
||||
Theme: () => import(/* webpackChunkName: "admin" */'../components/admin/Theme.vue')
|
||||
},
|
||||
|
@ -82,9 +102,9 @@ export default {
|
|||
try {
|
||||
const users = await $axios.$get('/users')
|
||||
const unconfirmedEvents = await $axios.$get('/event/unconfirmed')
|
||||
return { users, unconfirmedEvents, selectedTab: 0, url }
|
||||
return { users, unconfirmedEvents, url }
|
||||
} catch (e) {
|
||||
return { users: [], unconfirmedEvents: [], selectedTab: 0, url }
|
||||
return { users: [], unconfirmedEvents: [], url }
|
||||
}
|
||||
},
|
||||
data () {
|
||||
|
@ -93,7 +113,6 @@ export default {
|
|||
users: [],
|
||||
description: '',
|
||||
unconfirmedEvents: [],
|
||||
selectedTab: 0
|
||||
}
|
||||
},
|
||||
head () {
|
||||
|
@ -103,7 +122,15 @@ export default {
|
|||
...mapState(['settings']),
|
||||
unconfirmedUsers () {
|
||||
return this.users.filter(u => !u.is_active)
|
||||
}
|
||||
},
|
||||
selectedTab: {
|
||||
set (tab) {
|
||||
this.$router.replace({ query: { ...this.$route.query, tab } })
|
||||
},
|
||||
get () {
|
||||
return this.$route.query.tab
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async updateUsers () {
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
<template lang='pug'>
|
||||
.d-flex.justify-space-around
|
||||
v-form.d-flex.justify-space-around(method='post' action='/oauth/authorize')
|
||||
v-card.mt-5(max-width='600px')
|
||||
v-card-title {{settings.title}} - {{$t('common.authorize')}}
|
||||
v-card-text
|
||||
h2 {{$auth.user.email}}
|
||||
input(name='transaction_id' :value='transactionID' type='hidden')
|
||||
u {{$auth.user.email}}
|
||||
|
||||
div
|
||||
p(v-html="$t('oauth.authorization_request', { app: client.name, instance_name: settings.title })")
|
||||
p(v-html="$t('oauth.authorization_request', { app: client, instance_name: settings.title })")
|
||||
ul.mb-2
|
||||
li(v-for="s in scope.split(' ')") {{$t(`oauth.scopes.${scope}`)}}
|
||||
span(v-html="$t('oauth.redirected_to', {url: $route.query.redirect_uri})")
|
||||
li {{$t(`oauth.scopes.${scope}`)}}
|
||||
span(v-html="$t('oauth.redirected_to', {url: redirect_uri})")
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(color='error' to='/') {{$t('common.cancel')}}
|
||||
v-btn(:href='authorizeURL' color='success') {{$t('common.authorize')}}
|
||||
v-btn(color='error' to='/' outlined) {{$t('common.cancel')}}
|
||||
v-btn(type='submit' color='success' outlined) {{$t('common.authorize')}}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -23,58 +26,12 @@ export default {
|
|||
layout: 'modal',
|
||||
middleware: ['auth'],
|
||||
async asyncData ({ $axios, query, error, req }) {
|
||||
const { client_id, redirect_uri, scope, response_type } = query
|
||||
let err = ''
|
||||
if (!client_id) {
|
||||
err = 'client_id is missing'
|
||||
}
|
||||
if (!redirect_uri) {
|
||||
err = 'redirect_uri is missing'
|
||||
}
|
||||
if (!scope || scope !== 'event:write') {
|
||||
err = 'scope is missing or wrong'
|
||||
}
|
||||
if (!response_type || response_type !== 'code') {
|
||||
err = 'response_type is missing or wrong'
|
||||
}
|
||||
|
||||
// retrieve client validity
|
||||
try {
|
||||
const client = await $axios.$get(`/client/${client_id}`)
|
||||
if (!client) {
|
||||
err = 'client not found'
|
||||
}
|
||||
if (err) {
|
||||
return error({ statusCode: 404, message: err })
|
||||
}
|
||||
return { client, redirect_uri, scope, response_type }
|
||||
} catch (e) {
|
||||
error({ statusCode: 400, message: 'Something goes wrong with OAuth authorization' })
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
client: { name: 'Test' }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['settings']),
|
||||
authorizeURL () {
|
||||
const { scope, response_type, client_id, redirect_uri, state } = this.$route.query
|
||||
const query = `client_id=${client_id}&response_type=${response_type}&scope=${scope}&redirect_uri=${redirect_uri}&state=${state}`
|
||||
return `oauth/authorize?${query}`
|
||||
}
|
||||
const { transactionID, client, scope, redirect_uri } = query
|
||||
return { transactionID, client, redirect_uri, scope }
|
||||
},
|
||||
computed: mapState(['settings']),
|
||||
head () {
|
||||
return { title: `${this.settings.title} - Authorize` }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
h4 img {
|
||||
max-height: 40px;
|
||||
border-radius: 20px;
|
||||
background-color: #333;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
</style>
|
||||
</script>
|
|
@ -6,7 +6,7 @@ v-container
|
|||
v-card-text(v-else v-html='about')
|
||||
v-card-actions(v-if='$auth.user && $auth.user.is_admin')
|
||||
v-spacer
|
||||
v-btn(color='primary' text
|
||||
v-btn(color='primary' outlined
|
||||
@click='save') {{$t('common.save')}}
|
||||
</template>
|
||||
<script>
|
||||
|
|
|
@ -4,7 +4,7 @@ v-container.container.pa-0.pa-md-3
|
|||
v-card-title
|
||||
h4 {{ edit ? $t('common.edit_event') : $t('common.add_event') }}
|
||||
v-spacer
|
||||
v-btn(link text color='primary' @click='openImportDialog = true')
|
||||
v-btn(outlined color='primary' @click='openImportDialog = true')
|
||||
<v-icon v-text='mdiFileImport'></v-icon> {{ $t('common.import') }}
|
||||
v-dialog(v-model='openImportDialog' :fullscreen='$vuetify.breakpoint.xsOnly')
|
||||
ImportDialog(@close='openImportDialog = false' @imported='eventImported')
|
||||
|
@ -33,7 +33,7 @@ v-container.container.pa-0.pa-md-3
|
|||
WhereInput(ref='where' v-model='event.place')
|
||||
|
||||
//- When
|
||||
DateInput(v-model='date' :event='event')
|
||||
DateInput(ref='when' v-model='date' :event='event')
|
||||
//- Description
|
||||
v-col.px-0(cols='12')
|
||||
Editor.px-3.ma-0(
|
||||
|
@ -91,17 +91,33 @@ export default {
|
|||
WhereInput,
|
||||
DateInput
|
||||
},
|
||||
validate({ store }) {
|
||||
return (store.state.auth.loggedIn || store.state.settings.allow_anon_event)
|
||||
validate({ store, params, error }) {
|
||||
// should we allow anon event?
|
||||
if(!store.state.settings.allow_anon_event && !store.state.auth.loggedIn) {
|
||||
return error({ statusCode: 401, message: 'Not allowed'})
|
||||
}
|
||||
|
||||
// do not allow edit to anon users
|
||||
if (params.edit && !store.state.auth.loggedIn) {
|
||||
return error({ statusCode: 401, message: 'Not allowed'})
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
},
|
||||
async asyncData({ params, $axios, error }) {
|
||||
async asyncData({ params, $axios, error, $auth, store }) {
|
||||
if (params.edit) {
|
||||
|
||||
const data = { event: { place: {}, media: [] } }
|
||||
data.id = params.edit
|
||||
data.edit = true
|
||||
let event
|
||||
try {
|
||||
event = await $axios.$get('/event/' + data.id)
|
||||
if (!$auth.user.is_admin && $auth.user.id !== event.userId) {
|
||||
error({ statusCode: 401, message: 'Not allowed' })
|
||||
return {}
|
||||
}
|
||||
} catch (e) {
|
||||
error({ statusCode: 404, message: 'Event not found!' })
|
||||
return {}
|
||||
|
@ -109,13 +125,15 @@ export default {
|
|||
|
||||
data.event.place.name = event.place.name
|
||||
data.event.place.address = event.place.address || ''
|
||||
const from = dayjs.unix(event.start_datetime)
|
||||
const due = event.end_datetime && dayjs.unix(event.end_datetime)
|
||||
data.date = {
|
||||
recurrent: event.recurrent,
|
||||
from: dayjs.unix(event.start_datetime).toDate(),
|
||||
due: dayjs.unix(event.end_datetime).toDate(),
|
||||
from: from.toDate(),
|
||||
due: due && due.toDate(),
|
||||
multidate: event.multidate,
|
||||
fromHour: true,
|
||||
dueHour: true
|
||||
fromHour: from.format('HH:mm'),
|
||||
dueHour: due && due.format('HH:mm')
|
||||
}
|
||||
|
||||
data.event.title = event.title
|
||||
|
@ -135,7 +153,7 @@ export default {
|
|||
valid: false,
|
||||
openImportDialog: false,
|
||||
event: {
|
||||
place: { name: '', address: '' },
|
||||
place: { name: '', address: '', latitude: null, longitude: null },
|
||||
title: '',
|
||||
description: '',
|
||||
tags: [],
|
||||
|
@ -161,6 +179,7 @@ export default {
|
|||
filteredTags() {
|
||||
if (!this.tagName) { return this.tags.slice(0, 10).map(t => t.tag) }
|
||||
const tagName = this.tagName.trim().toLowerCase()
|
||||
console.log(tagName)
|
||||
return this.tags.filter(t => t.tag.toLowerCase().includes(tagName)).map(t => t.tag)
|
||||
}
|
||||
},
|
||||
|
@ -172,14 +191,17 @@ export default {
|
|||
}, 100),
|
||||
eventImported(event) {
|
||||
this.event = Object.assign(this.event, event)
|
||||
this.$refs.where.selectPlace({ name: event.place.name || event.place, create: true })
|
||||
|
||||
this.$refs.where.selectPlace({ name: event.place.name || event.place, address: event.place.address })
|
||||
const from = dayjs.unix(this.event.start_datetime)
|
||||
const due = this.event.end_datetime && dayjs.unix(this.event.end_datetime)
|
||||
this.date = {
|
||||
recurrent: this.event.recurrent || null,
|
||||
from: new Date(dayjs.unix(this.event.start_datetime)),
|
||||
due: new Date(dayjs.unix(this.event.end_datetime)),
|
||||
from: from.toDate(),
|
||||
due: due && due.toDate(),
|
||||
multidate: event.multidate,
|
||||
fromHour: true,
|
||||
dueHour: true
|
||||
fromHour: from.format('HH:mm'),
|
||||
dueHour: due && due.format('HH:mm')
|
||||
}
|
||||
this.openImportDialog = false
|
||||
},
|
||||
|
@ -212,12 +234,18 @@ export default {
|
|||
if (this.event.place.id) {
|
||||
formData.append('place_id', this.event.place.id)
|
||||
}
|
||||
formData.append('place_name', this.event.place.name)
|
||||
formData.append('place_name', this.event.place.name.trim())
|
||||
formData.append('place_address', this.event.place.address)
|
||||
formData.append('place_latitude', this.event.place.latitude)
|
||||
formData.append('place_longitude', this.event.place.longitude)
|
||||
formData.append('description', this.event.description)
|
||||
formData.append('multidate', !!this.date.multidate)
|
||||
formData.append('start_datetime', dayjs(this.date.from).unix())
|
||||
formData.append('end_datetime', this.date.due ? dayjs(this.date.due).unix() : dayjs(this.date.from).add(2, 'hour').unix())
|
||||
let [hour, minute] = this.date.fromHour.split(':')
|
||||
formData.append('start_datetime', dayjs(this.date.from).hour(Number(hour)).minute(Number(minute)).second(0).unix())
|
||||
if (this.date.dueHour) {
|
||||
[hour, minute] = this.date.dueHour.split(':')
|
||||
formData.append('end_datetime', dayjs(this.date.due).hour(Number(hour)).minute(Number(minute)).second(0).unix())
|
||||
}
|
||||
|
||||
if (this.edit) {
|
||||
formData.append('id', this.event.id)
|
||||
|
|
|
@ -4,11 +4,8 @@ v-container#event.pa-0.pa-sm-2
|
|||
//- gancio supports microformats (http://microformats.org/wiki/h-event)
|
||||
//- and microdata https://schema.org/Event
|
||||
v-card.h-event(itemscope itemtype="https://schema.org/Event")
|
||||
v-card-actions
|
||||
//- admin controls
|
||||
EventAdmin.mb-1(v-if='is_mine' :event='event')
|
||||
v-card-text
|
||||
|
||||
v-card-text
|
||||
v-row
|
||||
v-col.col-12.col-md-8
|
||||
MyPicture(v-if='hasMedia' :event='event')
|
||||
|
@ -18,39 +15,74 @@ v-container#event.pa-0.pa-sm-2
|
|||
v-card(outlined)
|
||||
v-card-text
|
||||
v-icon.float-right(v-if='event.parentId' color='success' v-text='mdiRepeat')
|
||||
.title.text-h5.mb-5
|
||||
.title.text-h5
|
||||
strong.p-name.text--primary(itemprop="name") {{event.title}}
|
||||
|
||||
time.dt-start.text-h6(:datetime='event.start_datetime|unixFormat("YYYY-MM-DD HH:mm")' itemprop="startDate" :content="event.start_datetime|unixFormat('YYYY-MM-DDTHH:mm')")
|
||||
v-icon(v-text='mdiCalendar')
|
||||
v-divider
|
||||
v-card-text
|
||||
time.dt-start.text-button(:datetime='event.start_datetime|unixFormat("YYYY-MM-DD HH:mm")' itemprop="startDate" :content="event.start_datetime|unixFormat('YYYY-MM-DDTHH:mm')")
|
||||
v-icon(v-text='mdiCalendar' small)
|
||||
strong.ml-2 {{event|when}}
|
||||
.d-none.dt-end(itemprop="endDate" :content="event.end_datetime|unixFormat('YYYY-MM-DDTHH:mm')") {{event.end_datetime|unixFormat('YYYY-MM-DD HH:mm')}}
|
||||
div.text-subtitle-1.mb-5 {{event.start_datetime|from}}
|
||||
div.text-caption.mb-3 {{event.start_datetime|from}}
|
||||
small(v-if='event.parentId') ({{event|recurrentDetail}})
|
||||
|
||||
.text-h6.p-location.h-adr(itemprop="location" itemscope itemtype="https://schema.org/Place")
|
||||
v-icon(v-text='mdiMapMarker')
|
||||
nuxt-link.vcard.ml-2.p-name.text-decoration-none(itemprop="name" :to='`/place/${event.place.name}`') {{event.place && event.place.name}}
|
||||
.text-subtitle-1.p-street-address(itemprop='address') {{event.place && event.place.address}}
|
||||
v-icon(v-text='mdiMapMarker' small)
|
||||
nuxt-link.vcard.ml-2.p-name.text-decoration-none.text-button(itemprop="name" :to='`/place/${encodeURIComponent(event.place.name)}`') {{event.place && event.place.name}}
|
||||
.text-caption.p-street-address(itemprop='address') {{event.place && event.place.address}}
|
||||
|
||||
//- tags, hashtags
|
||||
v-card-text.pt-0(v-if='event.tags && event.tags.length')
|
||||
v-chip.p-category.ml-1.mt-3(v-for='tag in event.tags' color='primary'
|
||||
outlined :key='tag' :to='`/tag/${tag}`') {{tag}}
|
||||
v-chip.p-category.ml-1.mt-1(v-for='tag in event.tags' small label color='primary'
|
||||
outlined :key='tag' :to='`/tag/${encodeURIComponent(tag)}`') {{tag}}
|
||||
|
||||
v-divider
|
||||
//- info & actions
|
||||
v-toolbar
|
||||
v-btn.ml-2(large icon :title="$t('common.copy_link')" :aria-label="$t('common.copy_link')" color='primary'
|
||||
@click='clipboard(`${settings.baseurl}/event/${event.slug || event.id}`)')
|
||||
v-icon(v-text='mdiContentCopy')
|
||||
v-btn.ml-2(large icon :title="$t('common.embed')" :aria-label="$t('common.embed')" @click='showEmbed=true' color='primary')
|
||||
v-icon(v-text='mdiCodeTags')
|
||||
v-btn.ml-2(large icon :title="$t('common.add_to_calendar')" color='primary' :aria-label="$t('common.add_to_calendar')"
|
||||
:href='`/api/event/${event.slug || event.id}.ics`')
|
||||
v-icon(v-text='mdiCalendarExport')
|
||||
v-btn.ml-2(v-if='hasMedia' large icon :title="$t('event.download_flyer')" color='primary' :aria-label="$t('event.download_flyer')"
|
||||
:href='event | mediaURL("download")')
|
||||
v-icon(v-text='mdiFileDownloadOutline')
|
||||
v-list(dense nav)
|
||||
//- v-list-group(:append-icon='mdiChevronUp' :value='true')
|
||||
//- template(v-slot:activator)
|
||||
//- v-list-item.text-overline {{$t('common.actions')}}
|
||||
|
||||
//- copy link
|
||||
v-list-item(@click='clipboard(`${settings.baseurl}/event/${event.slug || event.id}`)')
|
||||
v-list-item-icon
|
||||
v-icon(v-text='mdiContentCopy')
|
||||
v-list-item-content
|
||||
v-list-item-title(v-text="$t('common.copy_link')")
|
||||
|
||||
//- map
|
||||
v-list-item(v-if='settings.allow_geolocation && event.place.latitude && event.place.longitude' @click="mapModal = true")
|
||||
v-list-item-icon
|
||||
v-icon(v-text='mdiMap')
|
||||
v-list-item-content
|
||||
v-list-item-title(v-text="$t('common.show_map')")
|
||||
|
||||
//- embed
|
||||
v-list-item(@click='showEmbed=true')
|
||||
v-list-item-icon
|
||||
v-icon(v-text='mdiCodeTags')
|
||||
v-list-item-content
|
||||
v-list-item-title(v-text="$t('common.embed')")
|
||||
|
||||
//- calendar
|
||||
v-list-item(:href='`/api/event/${event.slug || event.id}.ics`')
|
||||
v-list-item-icon
|
||||
v-icon(v-text='mdiCalendarExport')
|
||||
v-list-item-content
|
||||
v-list-item-title(v-text="$t('common.add_to_calendar')")
|
||||
|
||||
//- download flyer
|
||||
v-list-item(v-if='hasMedia' :href='event | mediaURL("download")')
|
||||
v-list-item-icon
|
||||
v-icon(v-text='mdiFileDownloadOutline')
|
||||
v-list-item-content
|
||||
v-list-item-title(v-text="$t('event.download_flyer')")
|
||||
|
||||
v-divider
|
||||
|
||||
//- admin actions
|
||||
eventAdmin(v-if='is_mine' :event='event')
|
||||
|
||||
|
||||
.p-description.text-body-1.pa-3.rounded(v-if='hasMedia && event.description' itemprop='description' v-html='event.description')
|
||||
|
||||
|
@ -122,6 +154,9 @@ v-container#event.pa-0.pa-sm-2
|
|||
|
||||
v-dialog(v-model='showEmbed' width='700px' :fullscreen='$vuetify.breakpoint.xsOnly')
|
||||
EmbedEvent(:event='event' @close='showEmbed=false')
|
||||
|
||||
v-dialog(v-show='settings.allow_geolocation && event.place.latitude && event.place.longitude' v-model='mapModal' :fullscreen='$vuetify.breakpoint.xsOnly' destroy-on-close)
|
||||
Map(:event='event' @close='mapModal=false')
|
||||
|
||||
</template>
|
||||
<script>
|
||||
|
@ -135,9 +170,9 @@ import EmbedEvent from '@/components/embedEvent'
|
|||
|
||||
const { htmlToText } = require('html-to-text')
|
||||
|
||||
import { mdiArrowLeft, mdiArrowRight, mdiDotsVertical, mdiCodeTags, mdiClose,
|
||||
import { mdiArrowLeft, mdiArrowRight, mdiDotsVertical, mdiCodeTags, mdiClose, mdiMap,
|
||||
mdiEye, mdiEyeOff, mdiDelete, mdiRepeat, mdiLock, mdiFileDownloadOutline,
|
||||
mdiCalendarExport, mdiCalendar, mdiContentCopy, mdiMapMarker } from '@mdi/js'
|
||||
mdiCalendarExport, mdiCalendar, mdiContentCopy, mdiMapMarker, mdiChevronUp } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
name: 'Event',
|
||||
|
@ -145,7 +180,8 @@ export default {
|
|||
components: {
|
||||
EventAdmin,
|
||||
EmbedEvent,
|
||||
MyPicture
|
||||
MyPicture,
|
||||
[process.client && 'Map']: () => import('@/components/Map.vue')
|
||||
},
|
||||
async asyncData ({ $axios, params, error }) {
|
||||
try {
|
||||
|
@ -158,12 +194,14 @@ export default {
|
|||
data () {
|
||||
return {
|
||||
mdiArrowLeft, mdiArrowRight, mdiDotsVertical, mdiCodeTags, mdiCalendarExport, mdiCalendar, mdiFileDownloadOutline,
|
||||
mdiMapMarker, mdiContentCopy, mdiClose, mdiDelete, mdiEye, mdiEyeOff, mdiRepeat, mdiLock,
|
||||
mdiMapMarker, mdiContentCopy, mdiClose, mdiDelete, mdiEye, mdiEyeOff, mdiRepeat, mdiLock, mdiMap, mdiChevronUp,
|
||||
currentAttachment: 0,
|
||||
event: {},
|
||||
diocane: '',
|
||||
showEmbed: false,
|
||||
showResources: false,
|
||||
selectedResource: { data: { attachment: [] } }
|
||||
selectedResource: { data: { attachment: [] } },
|
||||
mapModal: false
|
||||
}
|
||||
},
|
||||
head () {
|
||||
|
@ -222,7 +260,7 @@ export default {
|
|||
{ property: 'twitter:title', content: this.event.title },
|
||||
{
|
||||
property: 'twitter:image',
|
||||
content: this.$options.filters.mediaURL(this.event, 'thumb')
|
||||
content: this.$options.filters.mediaURL(this.event)
|
||||
},
|
||||
{
|
||||
property: 'twitter:description',
|
||||
|
@ -230,7 +268,7 @@ export default {
|
|||
}
|
||||
],
|
||||
link: [
|
||||
{ rel: 'image_src', href: this.$options.filters.mediaURL(this.event, 'thumb') },
|
||||
{ rel: 'image_src', href: this.$options.filters.mediaURL(this.event) },
|
||||
{
|
||||
rel: 'alternate',
|
||||
type: 'application/rss+xml',
|
||||
|
@ -332,3 +370,5 @@ export default {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
|
133
pages/index.vue
133
pages/index.vue
|
@ -1,70 +1,47 @@
|
|||
<template lang="pug">
|
||||
v-container.pa-0
|
||||
v-container.px-2.px-sm-6.pt-0
|
||||
|
||||
//- Announcements
|
||||
#announcements.ma-2(v-if='announcements.length')
|
||||
#announcements.mt-2.mt-sm-4(v-if='announcements.length')
|
||||
Announcement(v-for='announcement in announcements' :key='`a_${announcement.id}`' :announcement='announcement')
|
||||
|
||||
//- Calendar and search bar
|
||||
v-row.ma-2
|
||||
#calh.col-xl-5.col-lg-5.col-md-7.col-sm-12.col-xs-12.pa-0.ma-0
|
||||
//- this is needed as v-calendar does not support SSR
|
||||
//- https://github.com/nathanreyes/v-calendar/issues/336
|
||||
client-only(placeholder='Loading...')
|
||||
Calendar(@dayclick='dayChange' @monthchange='monthChange' :events='events')
|
||||
|
||||
.col.pt-0.pt-md-2.mt-4.ma-md-0.pb-0
|
||||
//- v-btn(to='/search' color='primary' ) {{$t('common.search')}}
|
||||
v-form(to='/search' action='/search' method='GET')
|
||||
v-col(cols=12)
|
||||
v-switch(
|
||||
v-if='settings.allow_recurrent_event'
|
||||
v-model='show_recurrent'
|
||||
inset color='primary'
|
||||
hide-details
|
||||
:label="$t('event.show_recurrent')")
|
||||
v-col.mb-4(cols=12)
|
||||
v-text-field(name='search' :label='$t("common.search")' outlined rounded hide-details :append-icon='mdiMagnify')
|
||||
v-chip(v-if='selectedDay' close :close-icon='mdiCloseCircle' @click:close='dayChange()') {{selectedDay}}
|
||||
|
||||
|
||||
//- Events
|
||||
#events.mb-2.mt-1.pl-1.pl-sm-2
|
||||
Event(:event='event' @destroy='destroy' v-for='(event, idx) in visibleEvents' :lazy='idx>2' :key='event.id')
|
||||
#events.mt-sm-4.mt-2
|
||||
Event(:event='event' v-for='(event, idx) in visibleEvents' :lazy='idx>2' :key='event.id')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { mapState, mapActions } from 'vuex'
|
||||
import debounce from 'lodash/debounce'
|
||||
import dayjs from 'dayjs'
|
||||
import Event from '@/components/Event'
|
||||
import Announcement from '@/components/Announcement'
|
||||
import Calendar from '@/components/Calendar'
|
||||
import { mdiMagnify, mdiCloseCircle } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
name: 'Index',
|
||||
components: { Event, Announcement, Calendar },
|
||||
components: { Event, Announcement },
|
||||
middleware: 'setup',
|
||||
async asyncData ({ $api }) {
|
||||
const events = await $api.getEvents({
|
||||
start: dayjs().startOf('month').unix(),
|
||||
end: null,
|
||||
show_recurrent: true
|
||||
})
|
||||
return { events }
|
||||
async fetch () {
|
||||
return this.getEvents()
|
||||
},
|
||||
activated() {
|
||||
if (this.$fetchState.timestamp <= Date.now() - 60000) {
|
||||
this.$fetch();
|
||||
}
|
||||
},
|
||||
data ({ $store }) {
|
||||
return {
|
||||
mdiMagnify, mdiCloseCircle,
|
||||
first: true,
|
||||
isCurrentMonth: true,
|
||||
now: dayjs().unix(),
|
||||
date: dayjs.tz().format('YYYY-MM-DD'),
|
||||
events: [],
|
||||
start: dayjs().startOf('month').unix(),
|
||||
end: null,
|
||||
searching: false,
|
||||
tmpEvents: [],
|
||||
selectedDay: null,
|
||||
show_recurrent: $store.state.settings.recurrent_event_visible
|
||||
show_recurrent: $store.state.settings.recurrent_event_visible,
|
||||
}
|
||||
},
|
||||
head () {
|
||||
|
@ -79,69 +56,85 @@ export default {
|
|||
{ property: 'og:image', content: this.settings.baseurl + '/logo.png' }
|
||||
],
|
||||
link: [
|
||||
{ rel: 'apple-touch-icon', href: this.settings.baseurl + '/logo.png' },
|
||||
{ rel: 'alternate', type: 'application/rss+xml', title: this.settings.title, href: this.settings.baseurl + '/feed/rss' },
|
||||
{ rel: 'alternate', type: 'text/calendar', title: this.settings.title, href: this.settings.baseurl + '/feed/ics' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['settings', 'announcements']),
|
||||
...mapState(['settings', 'announcements', 'events']),
|
||||
visibleEvents () {
|
||||
if (this.searching) {
|
||||
return this.tmpEvents
|
||||
}
|
||||
const now = dayjs().unix()
|
||||
if (this.selectedDay) {
|
||||
const min = dayjs.tz(this.selectedDay).startOf('day').unix()
|
||||
const max = dayjs.tz(this.selectedDay).endOf('day').unix()
|
||||
return this.events.filter(e => (e.start_datetime <= max && (e.end_datetime || e.start_datetime) >= min) && (this.show_recurrent || !e.parentId))
|
||||
} else if (this.isCurrentMonth) {
|
||||
return this.events.filter(e => ((e.end_datetime ? e.end_datetime > now : e.start_datetime + 2 * 60 * 60 > now) && (this.show_recurrent || !e.parentId)))
|
||||
return this.events.filter(e => ((e.end_datetime ? e.end_datetime > now : e.start_datetime + 3 * 60 * 60 > now) && (this.show_recurrent || !e.parentId)))
|
||||
} else {
|
||||
return this.events.filter(e => this.show_recurrent || !e.parentId)
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$root.$on('dayclick', this.dayChange)
|
||||
this.$root.$on('monthchange', this.monthChange)
|
||||
this.$root.$on('search', debounce(this.search, 100))
|
||||
},
|
||||
destroyed () {
|
||||
this.$root.$off('dayclick')
|
||||
this.$root.$off('monthchange')
|
||||
this.$root.$off('search')
|
||||
},
|
||||
methods: {
|
||||
destroy (id) {
|
||||
this.events = this.events.filter(e => e.id !== id)
|
||||
...mapActions(['getEvents']),
|
||||
async search (query) {
|
||||
if (query) {
|
||||
this.tmpEvents = await this.$axios.$get(`/event/search?search=${query}`)
|
||||
this.searching = true
|
||||
} else {
|
||||
this.tmpEvents = null
|
||||
this.searching = false
|
||||
}
|
||||
},
|
||||
updateEvents () {
|
||||
this.events = []
|
||||
return this.$api.getEvents({
|
||||
return this.getEvents({
|
||||
start: this.start,
|
||||
end: this.end,
|
||||
show_recurrent: true
|
||||
}).then(events => {
|
||||
this.events = events
|
||||
this.$nuxt.$loading.finish()
|
||||
})
|
||||
},
|
||||
monthChange ({ year, month }) {
|
||||
// avoid first time monthChange event (onload)
|
||||
if (this.first) {
|
||||
this.first = false
|
||||
return
|
||||
}
|
||||
async monthChange ({ year, month }) {
|
||||
|
||||
this.$nuxt.$loading.start()
|
||||
this.$nextTick( async () => {
|
||||
|
||||
// unselect current selected day
|
||||
this.selectedDay = null
|
||||
// unselect current selected day
|
||||
this.selectedDay = null
|
||||
|
||||
// check if current month is selected
|
||||
if (month - 1 === dayjs.tz().month() && year === dayjs.tz().year()) {
|
||||
this.isCurrentMonth = true
|
||||
this.start = dayjs().startOf('month').unix()
|
||||
this.date = dayjs.tz().format('YYYY-MM-DD')
|
||||
} else {
|
||||
this.isCurrentMonth = false
|
||||
this.date = ''
|
||||
this.start = dayjs().year(year).month(month - 1).startOf('month').unix() // .startOf('week').unix()
|
||||
}
|
||||
this.end = dayjs().year(year).month(month).endOf('month').unix() // .endOf('week').unix()
|
||||
await this.updateEvents()
|
||||
this.$nuxt.$loading.finish()
|
||||
})
|
||||
|
||||
// check if current month is selected
|
||||
if (month - 1 === dayjs.tz().month() && year === dayjs.tz().year()) {
|
||||
this.isCurrentMonth = true
|
||||
this.start = dayjs().startOf('month').unix()
|
||||
this.date = dayjs.tz().format('YYYY-MM-DD')
|
||||
} else {
|
||||
this.isCurrentMonth = false
|
||||
this.date = ''
|
||||
this.start = dayjs().year(year).month(month - 1).startOf('month').unix() // .startOf('week').unix()
|
||||
}
|
||||
this.end = dayjs().year(year).month(month).endOf('month').unix() // .endOf('week').unix()
|
||||
this.updateEvents()
|
||||
},
|
||||
dayChange (day) {
|
||||
this.selectedDay = day ? dayjs.tz(day).format('YYYY-MM-DD') : null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
|
@ -33,7 +33,7 @@ export default {
|
|||
asyncData({ $axios, params, error }) {
|
||||
try {
|
||||
const place = params.place
|
||||
return $axios.$get(`/place/${place}`)
|
||||
return $axios.$get(`/place/${encodeURIComponent(place)}`)
|
||||
} catch (e) {
|
||||
error({ statusCode: 400, message: 'Error!' })
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ export default {
|
|||
async asyncData ({ $axios, params, error }) {
|
||||
try {
|
||||
const tag = params.tag
|
||||
const events = await $axios.$get(`/tag/${tag}`)
|
||||
const events = await $axios.$get(`/tag/${encodeURIComponent(tag)}`)
|
||||
return { events, tag }
|
||||
} catch (e) {
|
||||
error({ statusCode: 400, message: 'Error!' })
|
||||
|
|
|
@ -16,6 +16,8 @@ import 'dayjs/locale/fr'
|
|||
import 'dayjs/locale/de'
|
||||
import 'dayjs/locale/gl'
|
||||
import 'dayjs/locale/sk'
|
||||
import 'dayjs/locale/pt'
|
||||
import 'dayjs/locale/zh'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.extend(utc)
|
||||
|
@ -26,21 +28,21 @@ export default ({ app, store }) => {
|
|||
// set timezone to instance_timezone!!
|
||||
// to show local time relative to event's place
|
||||
// not where in the world I'm looking at the page from
|
||||
app.i18n.defaultLocale = store.state.settings.instance_locale
|
||||
const instance_timezone = store.state.settings.instance_timezone
|
||||
const locale = store.state.locale
|
||||
dayjs.tz.setDefault(instance_timezone)
|
||||
dayjs.locale(locale)
|
||||
dayjs.locale(app.i18n.locale || store.state.settings.instance_locale)
|
||||
|
||||
// replace links with anchors
|
||||
// TODO: remove fb tracking id?
|
||||
Vue.filter('linkify', value => value.replace(/(https?:\/\/([^\s]+))/g, '<a href="$1">$2</a>'))
|
||||
Vue.filter('url2host', url => url.match(/^https?:\/\/(.[^/:]+)/i)[1])
|
||||
Vue.filter('datetime', value => dayjs.tz(value).locale(locale).format('ddd, D MMMM HH:mm'))
|
||||
Vue.filter('datetime', value => dayjs.tz(value).locale(app.i18n.locale || store.state.settings.instance_locale).format('ddd, D MMMM HH:mm'))
|
||||
Vue.filter('dateFormat', (value, format) => dayjs.tz(value).format(format))
|
||||
Vue.filter('unixFormat', (timestamp, format) => dayjs.unix(timestamp).tz().format(format))
|
||||
|
||||
// shown in mobile homepage
|
||||
Vue.filter('day', value => dayjs.unix(value).tz().locale(store.state.locale).format('dddd, D MMM'))
|
||||
Vue.filter('day', value => dayjs.unix(value).tz().locale(app.i18n.locale || store.state.settings.instance_locale).format('dddd, D MMM'))
|
||||
Vue.filter('mediaURL', (event, type, format = '.jpg') => {
|
||||
const mediaPath = type === 'download' ? '/download/' : '/media/'
|
||||
if (event.media && event.media.length) {
|
||||
|
@ -59,6 +61,7 @@ export default ({ app, store }) => {
|
|||
|
||||
Vue.filter('recurrentDetail', event => {
|
||||
const parent = event.parent
|
||||
if (!parent.recurrent || !parent.recurrent.frequency) return 'error!'
|
||||
const { frequency, type } = parent.recurrent
|
||||
let recurrent
|
||||
if (frequency === '1w' || frequency === '2w') {
|
||||
|
@ -76,15 +79,13 @@ export default ({ app, store }) => {
|
|||
})
|
||||
|
||||
Vue.filter('when', (event) => {
|
||||
const start = dayjs.unix(event.start_datetime).tz()
|
||||
const end = dayjs.unix(event.end_datetime).tz()
|
||||
const start = dayjs.unix(event.start_datetime).tz().locale(app.i18n.locale || store.state.settings.instance_locale)
|
||||
const end = event.end_datetime && dayjs.unix(event.end_datetime).tz().locale(app.i18n.locale || store.state.settings.instance_locale)
|
||||
|
||||
// multidate
|
||||
if (event.multidate) {
|
||||
return `${start.format('dddd D MMMM HH:mm')} - ${end.format('dddd D MMMM HH:mm')}`
|
||||
let time = start.format('dddd D MMMM HH:mm')
|
||||
if (end) {
|
||||
time += event.multidate ? ` → ${end.format('dddd D MMMM HH:mm')}` : `-${end.format('HH:mm')}`
|
||||
}
|
||||
|
||||
// normal event
|
||||
return `${start.format('dddd D MMMM HH:mm')}-${end.format('HH:mm')}`
|
||||
return time
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,35 +1,36 @@
|
|||
import Vue from 'vue'
|
||||
import VueI18n from 'vue-i18n'
|
||||
import merge from 'lodash/merge'
|
||||
export default async ({ app, store, res, $vuetify }) => {
|
||||
|
||||
Vue.use(VueI18n)
|
||||
$vuetify.lang.current = app.i18n.locale
|
||||
|
||||
export default async ({ app, store, res }) => {
|
||||
const messages = {}
|
||||
if (process.server) {
|
||||
if (res.locals) {
|
||||
store.commit('setLocale', res.locals.acceptedLocale)
|
||||
if (res.locals.user_locale) {
|
||||
store.commit('setUserLocale', res.locals.user_locale)
|
||||
}
|
||||
}
|
||||
app.i18n.onLanguageSwitched = (oldLocale, newLocale) => {
|
||||
$vuetify.lang.current = newLocale
|
||||
}
|
||||
|
||||
messages[store.state.locale] = await import(/* webpackChunkName: "lang-[request]" */`../locales/${store.state.locale}.json`)
|
||||
// const messages = {}
|
||||
// if (process.server) {
|
||||
// if (res.locals) {
|
||||
// store.commit('setLocale', res.locals.acceptedLocale)
|
||||
// if (res.locals.user_locale) {
|
||||
// store.commit('setUserLocale', res.locals.user_locale)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// messages[store.state.locale] = await import(/* webpackChunkName: "lang-[request]" */`../locales/${store.state.locale}.json`)
|
||||
|
||||
// always include en fallback locale
|
||||
if (store.state.locale !== 'en') {
|
||||
messages.en = await import('../locales/en.json')
|
||||
}
|
||||
// if (store.state.locale !== 'en') {
|
||||
// messages.en = await import('../locales/en.json')
|
||||
// }
|
||||
|
||||
if (store.state.user_locale) {
|
||||
merge(messages[store.state.locale], store.state.user_locale)
|
||||
}
|
||||
// if (store.state.user_locale) {
|
||||
// merge(messages[store.state.locale], store.state.user_locale)
|
||||
// }
|
||||
|
||||
// Set i18n instance on app
|
||||
app.i18n = new VueI18n({
|
||||
locale: store.state.locale,
|
||||
fallbackLocale: 'en',
|
||||
messages
|
||||
})
|
||||
// app.i18n = new VueI18n({
|
||||
// locale: store.state.locale,
|
||||
// fallbackLocale: 'en',
|
||||
// messages
|
||||
// })
|
||||
}
|
||||
|
|
|
@ -1,28 +1,10 @@
|
|||
const log = require('../log')
|
||||
const oauth = require('./oauth')
|
||||
const get = require('lodash/get')
|
||||
|
||||
const Auth = {
|
||||
|
||||
fillUser (req, res, next) {
|
||||
const token = get(req.cookies, 'auth._token.local', null)
|
||||
const authorization = get(req.headers, 'authorization', null)
|
||||
if (!authorization && token) {
|
||||
req.headers.authorization = token
|
||||
}
|
||||
|
||||
if (!authorization && !token) {
|
||||
return next()
|
||||
}
|
||||
|
||||
oauth.oauthServer.authenticate()(req, res, () => {
|
||||
res.locals.user = get(res, 'locals.oauth.token.user', null)
|
||||
next()
|
||||
})
|
||||
},
|
||||
|
||||
isAuth (_req, res, next) {
|
||||
if (res.locals.user) {
|
||||
isAuth (req, res, next) {
|
||||
// TODO: check anon user
|
||||
if (req.user) {
|
||||
next()
|
||||
} else {
|
||||
res.sendStatus(403)
|
||||
|
@ -30,7 +12,7 @@ const Auth = {
|
|||
},
|
||||
|
||||
isAdmin (req, res, next) {
|
||||
if (res.locals.user && res.locals.user.is_admin) {
|
||||
if (req.user && req.user.is_admin) {
|
||||
next()
|
||||
} else {
|
||||
res.sendStatus(403)
|
||||
|
|
|
@ -23,6 +23,32 @@ const log = require('../../log')
|
|||
|
||||
const eventController = {
|
||||
|
||||
async _findOrCreatePlace (body) {
|
||||
if (body.place_id) {
|
||||
const place = await Place.findByPk(body.place_id)
|
||||
if (!place) {
|
||||
throw new Error(`Place not found`)
|
||||
}
|
||||
return place
|
||||
}
|
||||
|
||||
const place_name = body.place_name && body.place_name.trim()
|
||||
const place_address = body.place_address && body.place_address.trim()
|
||||
if (!place_address || !place_name) {
|
||||
throw new Error(`place_id or place_name and place_address are required`)
|
||||
}
|
||||
let place = await Place.findOne({ where: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), Sequelize.Op.eq, place_name.toLocaleLowerCase()) })
|
||||
if (!place) {
|
||||
place = await Place.create({
|
||||
name: place_name,
|
||||
address: place_address,
|
||||
latitude: body.place_latitude,
|
||||
longitude: body.place_longitude
|
||||
})
|
||||
}
|
||||
return place
|
||||
},
|
||||
|
||||
async searchMeta(req, res) {
|
||||
const search = req.query.search
|
||||
|
||||
|
@ -34,7 +60,7 @@ const eventController = {
|
|||
Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('address')), 'LIKE', '%' + search + '%')
|
||||
]
|
||||
},
|
||||
attributes: [['name', 'label'], 'address', 'id', [Sequelize.cast(Sequelize.fn('COUNT', Sequelize.col('events.placeId')), 'INTEGER'), 'w']],
|
||||
attributes: [['name', 'label'], 'address', 'latitude', 'longitude', 'id', [Sequelize.cast(Sequelize.fn('COUNT', Sequelize.col('events.placeId')), 'INTEGER'), 'w']],
|
||||
include: [{ model: Event, where: { is_visible: true }, required: true, attributes: [] }],
|
||||
group: ['place.id'],
|
||||
raw: true
|
||||
|
@ -110,7 +136,7 @@ const eventController = {
|
|||
attributes: ['tag'],
|
||||
through: { attributes: [] }
|
||||
},
|
||||
{ model: Place, required: true, attributes: ['id', 'name', 'address'] }
|
||||
{ model: Place, required: true, attributes: ['id', 'name', 'address', 'latitude', 'longitude'] }
|
||||
],
|
||||
replacements,
|
||||
limit: 30,
|
||||
|
@ -172,7 +198,7 @@ const eventController = {
|
|||
|
||||
async get(req, res) {
|
||||
const format = req.params.format || 'json'
|
||||
const is_admin = res.locals.user && res.locals.user.is_admin
|
||||
const is_admin = req.user && req.user.is_admin
|
||||
const slug = req.params.event_slug
|
||||
|
||||
// retrocompatibility, old events URL does not use slug, use id as fallback
|
||||
|
@ -192,7 +218,7 @@ const eventController = {
|
|||
},
|
||||
include: [
|
||||
{ model: Tag, required: false, attributes: ['tag'], through: { attributes: [] } },
|
||||
{ model: Place, attributes: ['name', 'address', 'id'] },
|
||||
{ model: Place, attributes: ['name', 'address', 'latitude', 'longitude', 'id'] },
|
||||
{
|
||||
model: Resource,
|
||||
where: !is_admin && { hidden: false },
|
||||
|
@ -277,7 +303,7 @@ const eventController = {
|
|||
log.warn(`Trying to confirm a unknown event, id: ${id}`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
if (!res.locals.user.is_admin && res.locals.user.id !== event.userId) {
|
||||
if (!req.user.is_admin && req.user.id !== event.userId) {
|
||||
log.warn(`Someone not allowed is trying to confirm -> "${event.title} `)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
@ -303,7 +329,7 @@ const eventController = {
|
|||
const id = Number(req.params.event_id)
|
||||
const event = await Event.findByPk(id)
|
||||
if (!event) { return req.sendStatus(404) }
|
||||
if (!res.locals.user.is_admin && res.locals.user.id !== event.userId) {
|
||||
if (!req.user.is_admin && req.user.id !== event.userId) {
|
||||
log.warn(`Someone not allowed is trying to unconfirm -> "${event.title} `)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
@ -362,8 +388,8 @@ const eventController = {
|
|||
res.sendStatus(200)
|
||||
},
|
||||
|
||||
async isAnonEventAllowed(_req, res, next) {
|
||||
if (!res.locals.settings.allow_anon_event && !res.locals.user) {
|
||||
async isAnonEventAllowed(req, res, next) {
|
||||
if (!res.locals.settings.allow_anon_event && !req.user) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
next()
|
||||
|
@ -389,29 +415,18 @@ const eventController = {
|
|||
|
||||
// find or create the place
|
||||
let place
|
||||
if (body.place_id) {
|
||||
place = await Place.findByPk(body.place_id)
|
||||
try {
|
||||
place = await eventController._findOrCreatePlace(body)
|
||||
if (!place) {
|
||||
return res.status(400).send(`Place not found`)
|
||||
}
|
||||
} else {
|
||||
if (!body.place_name) {
|
||||
return res.status(400).send(`Place not found`)
|
||||
}
|
||||
place = await Place.findOne({ where: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), Op.eq, body.place_name.trim().toLocaleLowerCase()) })
|
||||
if (!place) {
|
||||
if (!body.place_address || !body.place_name) {
|
||||
return res.status(400).send(`place_id or place_name and place_address required`)
|
||||
}
|
||||
place = await Place.create({
|
||||
name: body.place_name,
|
||||
address: body.place_address
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
return res.status(400).send(e.message)
|
||||
}
|
||||
|
||||
|
||||
const eventDetails = {
|
||||
title: body.title,
|
||||
title: body.title.trim(),
|
||||
// sanitize and linkify html
|
||||
description: helpers.sanitizeHTML(linkifyHtml(body.description || '')),
|
||||
multidate: body.multidate,
|
||||
|
@ -419,7 +434,7 @@ const eventController = {
|
|||
end_datetime: body.end_datetime,
|
||||
recurrent,
|
||||
// publish this event only if authenticated
|
||||
is_visible: !!res.locals.user
|
||||
is_visible: !!req.user
|
||||
}
|
||||
|
||||
if (req.file || body.image_url) {
|
||||
|
@ -453,9 +468,9 @@ const eventController = {
|
|||
}
|
||||
|
||||
// associate user to event and reverse
|
||||
if (res.locals.user) {
|
||||
await res.locals.user.addEvent(event)
|
||||
await event.setUser(res.locals.user)
|
||||
if (req.user) {
|
||||
await req.user.addEvent(event)
|
||||
await event.setUser(req.user)
|
||||
}
|
||||
|
||||
event = event.get()
|
||||
|
@ -489,7 +504,7 @@ const eventController = {
|
|||
const body = req.body
|
||||
const event = await Event.findByPk(body.id)
|
||||
if (!event) { return res.sendStatus(404) }
|
||||
if (!res.locals.user.is_admin && event.userId !== res.locals.user.id) {
|
||||
if (!req.user.is_admin && event.userId !== req.user.id) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
|
@ -500,7 +515,7 @@ const eventController = {
|
|||
description: helpers.sanitizeHTML(linkifyHtml(body.description || '', { target: '_blank' })) || event.description,
|
||||
multidate: body.multidate,
|
||||
start_datetime: body.start_datetime || event.start_datetime,
|
||||
end_datetime: body.end_datetime,
|
||||
end_datetime: body.end_datetime || null,
|
||||
recurrent
|
||||
}
|
||||
|
||||
|
@ -543,27 +558,14 @@ const eventController = {
|
|||
|
||||
// find or create the place
|
||||
let place
|
||||
if (body.place_id) {
|
||||
place = await Place.findByPk(body.place_id)
|
||||
try {
|
||||
place = await eventController._findOrCreatePlace(body)
|
||||
if (!place) {
|
||||
return res.status(400).send(`Place not found`)
|
||||
}
|
||||
} else {
|
||||
if (!body.place_name) {
|
||||
return res.status(400).send(`Place not found`)
|
||||
}
|
||||
place = await Place.findOne({ where: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), Op.eq, body.place_name.trim().toLocaleLowerCase()) })
|
||||
if (!place) {
|
||||
if (!body.place_address || !body.place_name) {
|
||||
return res.status(400).send(`place_id or place_name and place_address required`)
|
||||
}
|
||||
place = await Place.create({
|
||||
name: body.place_name,
|
||||
address: body.place_address
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
return res.status(400).send(e.message)
|
||||
}
|
||||
|
||||
await event.setPlace(place)
|
||||
|
||||
// create/assign tags
|
||||
|
@ -596,7 +598,7 @@ const eventController = {
|
|||
async remove(req, res) {
|
||||
const event = await Event.findByPk(req.params.id)
|
||||
// check if event is mine (or user is admin)
|
||||
if (event && (res.locals.user.is_admin || res.locals.user.id === event.userId)) {
|
||||
if (event && (req.user.is_admin || req.user.id === event.userId)) {
|
||||
if (event.media && event.media.length && !event.recurrent) {
|
||||
try {
|
||||
const old_path = path.join(config.upload_path, event.media[0].url)
|
||||
|
@ -624,7 +626,7 @@ const eventController = {
|
|||
|
||||
/**
|
||||
* Method to search for events with pagination and filtering
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
async _select({
|
||||
start = dayjs().unix(),
|
||||
|
@ -698,7 +700,7 @@ const eventController = {
|
|||
attributes: ['tag'],
|
||||
through: { attributes: [] }
|
||||
},
|
||||
{ model: Place, required: true, attributes: ['id', 'name', 'address'] }
|
||||
{ model: Place, required: true, attributes: ['id', 'name', 'address', 'latitude', 'longitude'] }
|
||||
],
|
||||
...pagination,
|
||||
replacements
|
||||
|
|
|
@ -88,6 +88,7 @@ const exportController = {
|
|||
const start = tmpStart.utc(true).format('YYYY-M-D-H-m').split('-').map(Number)
|
||||
const end = tmpEnd.utc(true).format('YYYY-M-D-H-m').split('-').map(Number)
|
||||
return {
|
||||
uid: `${e.id}@${settings.hostname}`,
|
||||
start,
|
||||
end,
|
||||
title: `[${settings.title}] ${e.title}`,
|
||||
|
|
|
@ -1,26 +1,357 @@
|
|||
const crypto = require('crypto')
|
||||
const { promisify } = require('util')
|
||||
const randomBytes = promisify(crypto.randomBytes)
|
||||
const bodyParser = require('body-parser')
|
||||
const cookieParser = require('cookie-parser')
|
||||
const session = require('cookie-session')
|
||||
|
||||
const OAuthClient = require('../models/oauth_client')
|
||||
const OAuthToken = require('../models/oauth_token')
|
||||
const OAuthCode = require('../models/oauth_code')
|
||||
|
||||
const helpers = require('../../helpers.js')
|
||||
const User = require('../models/user')
|
||||
const passport = require('passport')
|
||||
|
||||
const get = require('lodash/get')
|
||||
|
||||
const BasicStrategy = require('passport-http').BasicStrategy
|
||||
const ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy
|
||||
const ClientPublicStrategy = require('passport-oauth2-client-public').Strategy
|
||||
const BearerStrategy = require('passport-http-bearer').Strategy
|
||||
const AnonymousStrategy = require('passport-anonymous').Strategy
|
||||
|
||||
const oauth2orize = require('oauth2orize')
|
||||
const log = require('../../log')
|
||||
const dayjs = require('dayjs')
|
||||
|
||||
async function randomString (len = 16) {
|
||||
const bytes = await randomBytes(len * 8)
|
||||
return crypto
|
||||
.createHash('sha1')
|
||||
.update(bytes)
|
||||
.digest('hex')
|
||||
passport.serializeUser((user, done) => done(null, user.id))
|
||||
|
||||
passport.deserializeUser(async (id, done) => {
|
||||
const user = await User.findByPk(id)
|
||||
done(null, user)
|
||||
})
|
||||
|
||||
/**
|
||||
* BasicStrategy & ClientPasswordStrategy
|
||||
*
|
||||
* These strategies are used to authenticate registered OAuth clients. They are
|
||||
* employed to protect the `token` endpoint, which consumers use to obtain
|
||||
* access tokens. The OAuth 2.0 specification suggests that clients use the
|
||||
* HTTP Basic scheme to authenticate. Use of the client password strategy
|
||||
* allows clients to send the same credentials in the request body (as opposed
|
||||
* to the `Authorization` header). While this approach is not recommended by
|
||||
* the specification, in practice it is quite common.
|
||||
*/
|
||||
async function verifyClient(client_id, client_secret, done) {
|
||||
const client = await OAuthClient.findByPk(client_id, { raw: true })
|
||||
if (!client) {
|
||||
return done(null, false)
|
||||
}
|
||||
if (client.client_secret && client_secret !== client.client_secret) {
|
||||
return done(null, false)
|
||||
}
|
||||
|
||||
if (client) { client.grants = ['authorization_code', 'password'] } //sure ?
|
||||
|
||||
return done(null, client)
|
||||
}
|
||||
|
||||
const oauthController = {
|
||||
async function verifyPublicClient (client_id, done) {
|
||||
if (client_id !== 'self') {
|
||||
return done(null, false)
|
||||
}
|
||||
try {
|
||||
|
||||
// create client => http:///gancio.org/oauth#create-client
|
||||
const client = await OAuthClient.findByPk(client_id, { raw: true })
|
||||
done(null, client)
|
||||
} catch (e) {
|
||||
done(null, { message: e.message })
|
||||
}
|
||||
}
|
||||
|
||||
passport.use(new AnonymousStrategy())
|
||||
passport.use(new BasicStrategy(verifyClient))
|
||||
passport.use(new ClientPasswordStrategy(verifyClient))
|
||||
passport.use(new ClientPublicStrategy(verifyPublicClient))
|
||||
|
||||
/**
|
||||
* BearerStrategy
|
||||
*
|
||||
* This strategy is used to authenticate either users or clients based on an access token
|
||||
* (aka a bearer token). If a user, they must have previously authorized a client
|
||||
* application, which is issued an access token to make requests on behalf of
|
||||
* the authorizing user.
|
||||
*/
|
||||
passport.use(new BearerStrategy({ passReqToCallback: true }, verifyToken))
|
||||
|
||||
async function verifyToken (req, accessToken, done) {
|
||||
const token = await OAuthToken.findByPk(accessToken,
|
||||
{ include: [{ model: User, attributes: { exclude: ['password'] } }, { model: OAuthClient, as: 'client' }] })
|
||||
|
||||
if (!token) return done(null, false)
|
||||
if (token.userId) {
|
||||
if (!token.user) {
|
||||
return done(null, false)
|
||||
}
|
||||
// To keep this example simple, restricted scopes are not implemented,
|
||||
// and this is just for illustrative purposes.
|
||||
done(null, token.user, { scope: '*' })
|
||||
} else {
|
||||
|
||||
// The request came from a client only since userId is null,
|
||||
// therefore the client is passed back instead of a user.
|
||||
if (!token.client) {
|
||||
return done(null, false)
|
||||
}
|
||||
// To keep this example simple, restricted scopes are not implemented,
|
||||
// and this is just for illustrative purposes.
|
||||
done(null, client, { scope: '*' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const oauthServer = oauth2orize.createServer()
|
||||
|
||||
|
||||
// Register serialization and deserialization functions.
|
||||
//
|
||||
// When a client redirects a user to user authorization endpoint, an
|
||||
// authorization transaction is initiated. To complete the transaction, the
|
||||
// user must authenticate and approve the authorization request. Because this
|
||||
// may involve multiple HTTP request/response exchanges, the transaction is
|
||||
// stored in the session.
|
||||
//
|
||||
// An application must supply serialization functions, which determine how the
|
||||
// client object is serialized into the session. Typically this will be a
|
||||
// simple matter of serializing the client's ID, and deserializing by finding
|
||||
// the client by ID from the database.
|
||||
oauthServer.serializeClient((client, done) => {
|
||||
done(null, client.id)
|
||||
})
|
||||
|
||||
oauthServer.deserializeClient(async (id, done) => {
|
||||
const client = await OAuthClient.findByPk(id)
|
||||
done(null, client)
|
||||
})
|
||||
|
||||
// Register supported grant types.
|
||||
//
|
||||
// OAuth 2.0 specifies a framework that allows users to grant client
|
||||
// applications limited access to their protected resources. It does this
|
||||
// through a process of the user granting access, and the client exchanging
|
||||
// the grant for an access token.
|
||||
|
||||
// Grant authorization codes. The callback takes the `client` requesting
|
||||
// authorization, the `redirectUri` (which is used as a verifier in the
|
||||
// subsequent exchange), the authenticated `user` granting access, and
|
||||
// their response, which contains approved scope, duration, etc. as parsed by
|
||||
// the application. The application issues a code, which is bound to these
|
||||
// values, and will be exchanged for an access token.
|
||||
|
||||
oauthServer.grant(oauth2orize.grant.code(async (client, redirect_uri, user, ares, done) => {
|
||||
const authorizationCode = helpers.randomString(16);
|
||||
await OAuthCode.create({
|
||||
redirect_uri,
|
||||
authorizationCode,
|
||||
clientId: client.id,
|
||||
userId: user.id,
|
||||
})
|
||||
return done(null, authorizationCode)
|
||||
}))
|
||||
|
||||
|
||||
// Grant implicit authorization. The callback takes the `client` requesting
|
||||
// authorization, the authenticated `user` granting access, and
|
||||
// their response, which contains approved scope, duration, etc. as parsed by
|
||||
// the application. The application issues a token, which is bound to these
|
||||
// values.
|
||||
|
||||
oauthServer.grant(oauth2orize.grant.token((client, user, ares, done) => {
|
||||
return oauthController.issueTokens(user.id, client.clientId, done)
|
||||
}))
|
||||
|
||||
|
||||
// Exchange authorization codes for access tokens. The callback accepts the
|
||||
// `client`, which is exchanging `code` and any `redirectUri` from the
|
||||
// authorization request for verification. If these values are validated, the
|
||||
// application issues an access token on behalf of the user who authorized the
|
||||
// code. The issued access token response can include a refresh token and
|
||||
// custom parameters by adding these to the `done()` call
|
||||
|
||||
oauthServer.exchange(oauth2orize.exchange.code(async (client, code, redirect_uri, done) => {
|
||||
const oauthCode = await OAuthCode.findByPk(code)
|
||||
if (!oauthCode || client.id !== oauthCode.clientId || client.redirectUris !== oauthCode.redirect_uri) {
|
||||
return done(null, false)
|
||||
}
|
||||
return oauthController.issueTokens(oauthCode.userId, oauthCode.clientId, done)
|
||||
}))
|
||||
|
||||
|
||||
|
||||
// Exchange user id and password for access tokens. The callback accepts the
|
||||
// `client`, which is exchanging the user's name and password from the
|
||||
// authorization request for verification. If these values are validated, the
|
||||
// application issues an access token on behalf of the user who authorized the code.
|
||||
oauthServer.exchange(oauth2orize.exchange.password(async (client, username, password, scope, done) => {
|
||||
// Validate the client
|
||||
const oauthClient = await OAuthClient.findByPk(client.id)
|
||||
if (!oauthClient) { // || oauthClient.client_secret !== client.clientSecret) {
|
||||
return done(null, false)
|
||||
}
|
||||
const user = await User.findOne({ where: { email: username, is_active: true } })
|
||||
if (!user) {
|
||||
return done(null, false)
|
||||
}
|
||||
// check if password matches
|
||||
if (await user.comparePassword(password)) {
|
||||
return oauthController.issueTokens(user.id, oauthClient.id, done)
|
||||
}
|
||||
return done(null, false)
|
||||
}))
|
||||
|
||||
|
||||
// Exchange the client id and password/secret for an access token. The callback accepts the
|
||||
// `client`, which is exchanging the client's id and password/secret from the
|
||||
// authorization request for verification. If these values are validated, the
|
||||
// application issues an access token on behalf of the client who authorized the code.
|
||||
oauthServer.exchange(oauth2orize.exchange.clientCredentials(async (client, scope, done) => {
|
||||
// Validate the client
|
||||
const oauthClient = await OAuthClient.findByPk(client.clientId)
|
||||
if (!oauthClient || oauthClient.client_secret !== client.clientSecret) {
|
||||
return done(null, false)
|
||||
}
|
||||
|
||||
return oauthController.issueTokens(null, oauthClient.id, done)
|
||||
}))
|
||||
|
||||
// issue new tokens and remove the old ones
|
||||
oauthServer.exchange(oauth2orize.exchange.refreshToken(async (client, refreshToken, scope, done) => {
|
||||
// db.refreshTokens.find(refreshToken, (error, token) => {
|
||||
// if (error) return done(error)
|
||||
// issueTokens(token.id, client.id, (err, accessToken, refreshToken) => {
|
||||
// if (err) {
|
||||
// done(err, null, null)
|
||||
// }
|
||||
// db.accessTokens.removeByUserIdAndClientId(token.userId, token.clientId, (err) => {
|
||||
// if (err) {
|
||||
// done(err, null, null)
|
||||
// }
|
||||
// db.refreshTokens.removeByUserIdAndClientId(token.userId, token.clientId, (err) => {
|
||||
// if (err) {
|
||||
// done(err, null, null)
|
||||
// }
|
||||
// done(null, accessToken, refreshToken)
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
}))
|
||||
|
||||
|
||||
const oauthController = {
|
||||
|
||||
// this is a middleware to authenticate a request
|
||||
authenticate: [
|
||||
passport.initialize(), // initialize passport
|
||||
cookieParser(), // parse cookies
|
||||
session({ secret: 'secret', resave: true, saveUninitialized: true }),
|
||||
passport.session(),
|
||||
(req, res, next) => { // retrocompatibility
|
||||
const token = get(req.cookies, 'auth._token.local', null)
|
||||
const authorization = get(req.headers, 'authorization', null)
|
||||
if (!authorization && token) {
|
||||
req.headers.authorization = token
|
||||
}
|
||||
next()
|
||||
},
|
||||
passport.authenticate(['bearer', 'oauth2-client-password', 'anonymous'], { session: false })
|
||||
],
|
||||
|
||||
login: [
|
||||
bodyParser.urlencoded({ extended: true }), // login is done via application/x-www-form-urlencoded form
|
||||
passport.authenticate(['oauth2-client-public'], { session: false }),
|
||||
oauthServer.token(),
|
||||
oauthServer.errorHandler()
|
||||
],
|
||||
|
||||
token: [
|
||||
bodyParser.urlencoded({ extended: true }), // login is done via application/x-www-form-urlencoded form
|
||||
passport.authenticate(['bearer', 'oauth2-client-password'], { session: false }),
|
||||
oauthServer.token(),
|
||||
oauthServer.errorHandler()
|
||||
],
|
||||
|
||||
authorization: [
|
||||
oauthServer.authorization(async (clientId, redirectUri, done) => {
|
||||
const oauthClient = await OAuthClient.findByPk(clientId)
|
||||
if (!oauthClient) {
|
||||
return done(null, false)
|
||||
}
|
||||
|
||||
// WARNING: For security purposes, it is highly advisable to check that
|
||||
// redirectUri provided by the client matches one registered with
|
||||
// the server. For simplicity, this example does not. You have
|
||||
// been warned.
|
||||
return done(null, oauthClient, redirectUri);
|
||||
}, async (client, user, done) => {
|
||||
// Check if grant request qualifies for immediate approval
|
||||
|
||||
// Auto-approve
|
||||
if (client.isTrusted) return done(null, true);
|
||||
if (!user) {
|
||||
return done(null, false)
|
||||
}
|
||||
const token = await OAuthToken.findOne({ where: { clientId: client.id, userId: user.id }})
|
||||
// Auto-approve
|
||||
if (token) {
|
||||
return done(null, true)
|
||||
}
|
||||
// Otherwise ask user
|
||||
return done(null, false)
|
||||
|
||||
}),
|
||||
(req, res, next) => {
|
||||
//clean old transactionID
|
||||
if(req.session.authorize){
|
||||
for(const key in req.session.authorize){
|
||||
if(key !== req.oauth2.transactionID){
|
||||
delete req.session.authorize[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
const query = new URLSearchParams({
|
||||
transactionID: req.oauth2.transactionID,
|
||||
client: req.oauth2.client.name,
|
||||
scope: req.oauth2.client.scopes,
|
||||
redirect_uri: req.oauth2.client.redirectUris
|
||||
})
|
||||
return res.redirect(`/authorize?${query.toString()}`)
|
||||
}
|
||||
],
|
||||
|
||||
decision: [
|
||||
bodyParser.urlencoded({ extended: true }),
|
||||
oauthServer.decision()
|
||||
],
|
||||
|
||||
async issueTokens(userId, clientId, done) {
|
||||
const user = await User.findByPk(userId)
|
||||
if (!user) {
|
||||
return done(null, false)
|
||||
}
|
||||
|
||||
const refreshToken = helpers.randomString(32)
|
||||
const accessToken = helpers.randomString(32)
|
||||
|
||||
const token = {
|
||||
refreshToken,
|
||||
accessToken,
|
||||
userId,
|
||||
clientId
|
||||
}
|
||||
|
||||
await OAuthToken.create(token)
|
||||
return done(null, accessToken, refreshToken, { username: user.email })
|
||||
},
|
||||
|
||||
// create client => http:///gancio.org/dev/oauth#create-client
|
||||
async createClient (req, res) {
|
||||
// only write scope is supported
|
||||
if (req.body.scopes && req.body.scopes !== 'event:write') {
|
||||
|
@ -28,12 +359,12 @@ const oauthController = {
|
|||
}
|
||||
|
||||
const client = {
|
||||
id: await randomString(256),
|
||||
id: helpers.randomString(32),
|
||||
name: req.body.client_name,
|
||||
redirectUris: req.body.redirect_uris,
|
||||
scopes: req.body.scopes || 'event:write',
|
||||
website: req.body.website,
|
||||
client_secret: await randomString(256)
|
||||
client_secret: helpers.randomString(32)
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -63,99 +394,11 @@ const oauthController = {
|
|||
|
||||
async getClients (req, res) {
|
||||
const tokens = await OAuthToken.findAll({
|
||||
include: [{ model: User, where: { id: res.locals.user.id } }, { model: OAuthClient, as: 'client' }],
|
||||
include: [{ model: User, where: { id: req.user.id } }, { model: OAuthClient, as: 'client' }],
|
||||
raw: true,
|
||||
nest: true
|
||||
})
|
||||
res.json(tokens)
|
||||
},
|
||||
|
||||
model: {
|
||||
|
||||
/**
|
||||
* Invoked to retrieve an existing access token previously saved through #saveToken().
|
||||
* https://oauth2-server.readthedocs.io/en/latest/model/spec.html#getaccesstoken-accesstoken-callback
|
||||
* */
|
||||
async getAccessToken (accessToken) {
|
||||
const oauth_token = await OAuthToken.findByPk(accessToken,
|
||||
{ include: [{ model: User, attributes: { exclude: ['password'] } }, { model: OAuthClient, as: 'client' }] })
|
||||
return oauth_token
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoked to retrieve a client using a client id or a client id/client secret combination, depend on the grant type.
|
||||
*/
|
||||
async getClient (client_id, client_secret) {
|
||||
const client = await OAuthClient.findByPk(client_id, { raw: true })
|
||||
if (client_secret && client_secret !== client.client_secret) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (client) { client.grants = ['authorization_code', 'password'] }
|
||||
|
||||
return client
|
||||
},
|
||||
|
||||
async getRefreshToken (refresh_token) {
|
||||
const oauth_token = await OAuthToken.findOne({ where: { refresh_token }, raw: true })
|
||||
return oauth_token
|
||||
},
|
||||
|
||||
async getAuthorizationCode (code) {
|
||||
const oauth_code = await OAuthCode.findByPk(code,
|
||||
{ include: [User, { model: OAuthClient, as: 'client' }] })
|
||||
return oauth_code
|
||||
},
|
||||
|
||||
async saveToken (token, client, user) {
|
||||
token.userId = user.id
|
||||
token.clientId = client.id
|
||||
const oauth_token = await OAuthToken.create(token)
|
||||
oauth_token.client = client
|
||||
oauth_token.user = user
|
||||
return oauth_token
|
||||
},
|
||||
|
||||
async revokeAuthorizationCode (code) {
|
||||
const oauth_code = await OAuthCode.findByPk(code.authorizationCode)
|
||||
return oauth_code.destroy()
|
||||
},
|
||||
|
||||
async getUser (username, password) {
|
||||
const user = await User.findOne({ where: { email: username } })
|
||||
if (!user || !user.is_active) {
|
||||
return false
|
||||
}
|
||||
// check if password matches
|
||||
if (await user.comparePassword(password)) {
|
||||
return user
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
async saveAuthorizationCode (code, client, user) {
|
||||
code.userId = user.id
|
||||
code.clientId = client.id
|
||||
code.expiresAt = dayjs(code.expiresAt).toDate()
|
||||
return OAuthCode.create(code)
|
||||
},
|
||||
|
||||
// TODO
|
||||
verifyScope (token, scope) {
|
||||
// const userScope = [
|
||||
// 'user:remove',
|
||||
// 'user:update',
|
||||
// 'event:write',
|
||||
// 'event:remove'
|
||||
// ]
|
||||
log.debug(`VERIFY SCOPE ${scope} ${token.user.email}`)
|
||||
if (token.user.is_admin && token.user.is_active) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,13 @@ const Event = require('../models/event')
|
|||
const eventController = require('./event')
|
||||
const exportController = require('./export')
|
||||
|
||||
const { version } = require('../../../package.json')
|
||||
|
||||
const log = require('../../log')
|
||||
const { Op, where, col, fn, cast } = require('sequelize')
|
||||
const NOMINATIM_URL = 'https://nominatim.openstreetmap.org/search'
|
||||
const PHOTON_URL = 'https://photon.komoot.io/api/'
|
||||
const axios = require('axios')
|
||||
|
||||
module.exports = {
|
||||
|
||||
|
@ -60,7 +65,7 @@ module.exports = {
|
|||
{ address: where(fn('LOWER', col('address')), 'LIKE', '%' + search + '%')},
|
||||
]
|
||||
},
|
||||
attributes: ['name', 'address', 'id'],
|
||||
attributes: ['name', 'address', 'latitude', 'longitude', 'id'],
|
||||
include: [{ model: Event, where: { is_visible: true }, required: true, attributes: [] }],
|
||||
group: ['place.id'],
|
||||
raw: true,
|
||||
|
@ -70,6 +75,45 @@ module.exports = {
|
|||
|
||||
// TOFIX: don't know why limit does not work
|
||||
return res.json(places.slice(0, 10))
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
async _nominatim (req, res) {
|
||||
const details = req.params.place_details
|
||||
const countrycodes = res.locals.settings.geocoding_countrycodes || []
|
||||
const geocoding_provider = res.locals.settings.geocoding_provider || NOMINATIM_URL
|
||||
// ?limit=3&format=json&namedetails=1&addressdetails=1&q=
|
||||
|
||||
const ret = await axios.get(`${geocoding_provider}`, {
|
||||
params: {
|
||||
countrycodes: countrycodes.join(','),
|
||||
q: details,
|
||||
limit: 3,
|
||||
format: 'json',
|
||||
addressdetails: 1,
|
||||
namedetails: 1,
|
||||
},
|
||||
headers: { 'User-Agent': `gancio ${version}` }
|
||||
})
|
||||
|
||||
return res.json(ret.data)
|
||||
|
||||
},
|
||||
|
||||
async _photon (req, res) {
|
||||
const details = req.params.place_details
|
||||
const geocoding_provider = res.locals.settings.geocoding_provider || PHOTON_URL
|
||||
|
||||
const ret = await axios.get(`${geocoding_provider}`, {
|
||||
params: {
|
||||
q: details,
|
||||
limit: 3,
|
||||
},
|
||||
headers: { 'User-Agent': `gancio ${version}` }
|
||||
})
|
||||
|
||||
// console.log(ret)
|
||||
return res.json(ret.data)
|
||||
|
||||
},
|
||||
|
||||
}
|
||||
|
|
121
server/api/controller/plugins.js
Normal file
121
server/api/controller/plugins.js
Normal file
|
@ -0,0 +1,121 @@
|
|||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const log = require('../../log')
|
||||
const config = require('../../config')
|
||||
|
||||
const pluginController = {
|
||||
plugins: [],
|
||||
getAll(_req, res) {
|
||||
const settingsController = require('./settings')
|
||||
// return plugins and inner settings
|
||||
const plugins = pluginController.plugins.map( ({ configuration }) => {
|
||||
if (settingsController.settings['plugin_' + configuration.name]) {
|
||||
configuration.settingsValue = settingsController.settings['plugin_' + configuration.name]
|
||||
}
|
||||
return configuration
|
||||
})
|
||||
return res.json(plugins)
|
||||
},
|
||||
|
||||
togglePlugin(req, res) {
|
||||
const settingsController = require('./settings')
|
||||
const pluginName = req.params.plugin
|
||||
const pluginSettings = settingsController.settings['plugin_' + pluginName]
|
||||
if (!pluginSettings) { return res.sendStatus(404) }
|
||||
if (!pluginSettings.enable) {
|
||||
pluginController.loadPlugin(pluginName)
|
||||
} else {
|
||||
pluginController.unloadPlugin(pluginName)
|
||||
}
|
||||
settingsController.set('plugin_' + pluginName,
|
||||
{ ...pluginSettings, enable: !pluginSettings.enable })
|
||||
res.json()
|
||||
},
|
||||
|
||||
unloadPlugin(pluginName) {
|
||||
const settingsController = require('./settings')
|
||||
const plugin = pluginController.plugins.find(p => p.configuration.name === pluginName)
|
||||
const settings = settingsController.settings['plugin_' + pluginName]
|
||||
if (!plugin) {
|
||||
log.warn(`Plugin ${pluginName} not found`)
|
||||
return
|
||||
}
|
||||
const notifier = require('../../notifier')
|
||||
log.info('Unload plugin ' + plugin)
|
||||
if (typeof plugin.onEventCreate === 'function') {
|
||||
notifier.emitter.off('Create', plugin.onEventCreate)
|
||||
}
|
||||
if (typeof plugin.onEventDelete === 'function') {
|
||||
notifier.emitter.off('Delete', plugin.onEventDelete)
|
||||
}
|
||||
if (typeof plugin.onEventUpdate === 'function') {
|
||||
notifier.emitter.off('Update', plugin.onEventUpdate)
|
||||
}
|
||||
|
||||
if (plugin.unload && typeof plugin.unload === 'function') {
|
||||
plugin.unload({ settings: settingsController.settings }, settings)
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
loadPlugin(pluginName) {
|
||||
const settingsController = require('./settings')
|
||||
const plugin = pluginController.plugins.find(p => p.configuration.name === pluginName)
|
||||
const settings = settingsController.settings['plugin_' + pluginName]
|
||||
if (!plugin) {
|
||||
log.warn(`Plugin ${pluginName} not found`)
|
||||
return
|
||||
}
|
||||
const notifier = require('../../notifier')
|
||||
log.info('Load plugin ' + pluginName)
|
||||
if (typeof plugin.onEventCreate === 'function') {
|
||||
notifier.emitter.on('Create', plugin.onEventCreate)
|
||||
}
|
||||
if (typeof plugin.onEventDelete === 'function') {
|
||||
notifier.emitter.on('Delete', plugin.onEventDelete)
|
||||
}
|
||||
if (typeof plugin.onEventUpdate === 'function') {
|
||||
notifier.emitter.on('Update', plugin.onEventUpdate)
|
||||
}
|
||||
|
||||
if (plugin.load && typeof plugin.load === 'function') {
|
||||
plugin.load({
|
||||
helpers: require('../../helpers'),
|
||||
settings: settingsController.settings
|
||||
},
|
||||
settings)
|
||||
}
|
||||
},
|
||||
|
||||
_load() {
|
||||
const settingsController = require('./settings')
|
||||
// load custom plugins
|
||||
const plugins_path = config.plugins_path || path.resolve(process.env.cwd || '', 'plugins')
|
||||
log.info(`Loading plugin ${plugins_path}`)
|
||||
if (fs.existsSync(plugins_path)) {
|
||||
const plugins = fs.readdirSync(plugins_path)
|
||||
.map(e => path.resolve(plugins_path, e, 'index.js'))
|
||||
.filter(index => fs.existsSync(index))
|
||||
plugins.forEach(pluginFile => {
|
||||
try {
|
||||
const plugin = require(pluginFile)
|
||||
const name = plugin.configuration.name
|
||||
console.log(`Found plugin '${name}'`)
|
||||
pluginController.plugins.push(plugin)
|
||||
if (settingsController.settings['plugin_' + name]) {
|
||||
const pluginSetting = settingsController.settings['plugin_' + name]
|
||||
if (pluginSetting.enable) {
|
||||
pluginController.loadPlugin(name)
|
||||
}
|
||||
} else {
|
||||
settingsController.set('plugin_' + name, { enable: false })
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn(`Unable to load plugin ${pluginFile}: ${String(e)}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = pluginController
|
|
@ -7,9 +7,9 @@ const sharp = require('sharp')
|
|||
const config = require('../../config')
|
||||
const generateKeyPair = promisify(crypto.generateKeyPair)
|
||||
const log = require('../../log')
|
||||
const locales = require('../../../locales/index')
|
||||
// const locales = require('../../../locales/index')
|
||||
const escape = require('lodash/escape')
|
||||
|
||||
const pluginController = require('./plugins')
|
||||
|
||||
let defaultHostname
|
||||
try {
|
||||
|
@ -27,8 +27,15 @@ const defaultSettings = {
|
|||
instance_place: '',
|
||||
allow_registration: true,
|
||||
allow_anon_event: true,
|
||||
allow_multidate_event: true,
|
||||
allow_recurrent_event: false,
|
||||
recurrent_event_visible: false,
|
||||
allow_geolocation: true,
|
||||
geocoding_provider_type: 'Nominatim',
|
||||
geocoding_provider: 'https://nominatim.openstreetmap.org/search',
|
||||
geocoding_countrycodes: [],
|
||||
tilelayer_provider: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
tilelayer_provider_attribution: "<a target=\"_blank\" href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors",
|
||||
enable_federation: true,
|
||||
enable_resources: false,
|
||||
hide_boosts: true,
|
||||
|
@ -36,10 +43,14 @@ const defaultSettings = {
|
|||
trusted_instances: [],
|
||||
'theme.is_dark': true,
|
||||
'theme.primary': '#FF4500',
|
||||
trusted_instances_label: '',
|
||||
hide_thumbs: false,
|
||||
hide_calendar: false,
|
||||
footerLinks: [
|
||||
{ href: '/', label: 'home' },
|
||||
{ href: '/about', label: 'about' }
|
||||
{ href: '/', label: 'common.home' },
|
||||
{ href: '/about', label: 'common.about' }
|
||||
],
|
||||
plugins: [],
|
||||
admin_email: config.admin_email || '',
|
||||
smtp: config.smtp || {}
|
||||
}
|
||||
|
@ -94,44 +105,20 @@ const settingsController = {
|
|||
}
|
||||
|
||||
// initialize user_locale
|
||||
if (config.user_locale && fs.existsSync(path.resolve(config.user_locale))) {
|
||||
const user_locales_files = fs.readdirSync(path.resolve(config.user_locale))
|
||||
user_locales_files.forEach( f => {
|
||||
const locale = path.basename(f ,'.json')
|
||||
if (locales[locale]) {
|
||||
log.info(`Adding custom locale ${locale}`)
|
||||
settingsController.user_locale[locale] = require(path.resolve(config.user_locale, f)).default
|
||||
} else {
|
||||
log.warning(`Unknown custom user locale: ${locale} [valid locales are ${locales}]`)
|
||||
}
|
||||
})
|
||||
}
|
||||
// if (config.user_locale && fs.existsSync(path.resolve(config.user_locale))) {
|
||||
// const user_locales_files = fs.readdirSync(path.resolve(config.user_locale))
|
||||
// user_locales_files.forEach( f => {
|
||||
// const locale = path.basename(f ,'.json')
|
||||
// if (locales[locale]) {
|
||||
// log.info(`Adding custom locale ${locale}`)
|
||||
// settingsController.user_locale[locale] = require(path.resolve(config.user_locale, f)).default
|
||||
// } else {
|
||||
// log.warning(`Unknown custom user locale: ${locale} [valid locales are ${locales}]`)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// load custom plugins
|
||||
const plugins_path = path.resolve(process.env.cwd || '', 'plugins')
|
||||
if (process.env.NODE_ENV === 'production' && fs.existsSync(plugins_path)) {
|
||||
const notifier = require('../../notifier')
|
||||
const pluginsFile = fs.readdirSync(plugins_path).filter(e => path.extname(e).toLowerCase() === '.js')
|
||||
pluginsFile.forEach( pluginFile => {
|
||||
try {
|
||||
const plugin = require(path.resolve(plugins_path, pluginFile))
|
||||
if (typeof plugin.load !== 'function') return
|
||||
plugin.load({ settings: settingsController.settings })
|
||||
log.info(`Plugin ${pluginFile} loaded!`)
|
||||
if (typeof plugin.onEventCreate === 'function') {
|
||||
notifier.emitter.on('Create', plugin.onEventCreate)
|
||||
}
|
||||
if (typeof plugin.onEventDelete === 'function') {
|
||||
notifier.emitter.on('Delete', plugin.onEventDelete)
|
||||
}
|
||||
if (typeof plugin.onEventUpdate === 'function') {
|
||||
notifier.emitter.on('Update', plugin.onEventUpdate)
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn(`Unable to load plugin ${pluginFile}: ${String(e)}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
pluginController._load()
|
||||
},
|
||||
|
||||
async set (key, value, is_secret = false) {
|
||||
|
@ -199,7 +186,52 @@ const settingsController = {
|
|||
settingsController.set('logo', baseImgPath)
|
||||
res.sendStatus(200)
|
||||
})
|
||||
},
|
||||
|
||||
setFallbackImage (req, res) {
|
||||
if (!req.file) {
|
||||
settingsController.set('fallback_image', false)
|
||||
return res.status(200)
|
||||
}
|
||||
|
||||
const uploadedPath = path.join(req.file.destination, req.file.filename)
|
||||
const baseImgPath = path.resolve(config.upload_path, 'fallbackImage.png')
|
||||
|
||||
// convert and resize to png
|
||||
return sharp(uploadedPath)
|
||||
.resize(600)
|
||||
.png({ quality: 99 })
|
||||
.toFile(baseImgPath, (err) => {
|
||||
if (err) {
|
||||
log.error('[FALLBACK IMAGE] ' + err)
|
||||
}
|
||||
settingsController.set('fallback_image', baseImgPath)
|
||||
res.sendStatus(200)
|
||||
})
|
||||
},
|
||||
|
||||
setHeaderImage (req, res) {
|
||||
if (!req.file) {
|
||||
settingsController.set('header_image', false)
|
||||
return res.status(200)
|
||||
}
|
||||
|
||||
const uploadedPath = path.join(req.file.destination, req.file.filename)
|
||||
const baseImgPath = path.resolve(config.upload_path, 'headerImage.png')
|
||||
|
||||
// convert and resize to png
|
||||
return sharp(uploadedPath)
|
||||
.resize(600)
|
||||
.png({ quality: 99 })
|
||||
.toFile(baseImgPath, (err) => {
|
||||
if (err) {
|
||||
log.error('[HEADER IMAGE] ' + err)
|
||||
}
|
||||
settingsController.set('header_image', baseImgPath)
|
||||
res.sendStatus(200)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = settingsController
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
const Tag = require('../models/tag')
|
||||
const Event = require('../models/event')
|
||||
const uniq = require('lodash/uniq')
|
||||
const log = require('../../log')
|
||||
|
||||
|
||||
const { where, fn, col, Op } = require('sequelize')
|
||||
const exportController = require('./export')
|
||||
|
@ -45,6 +47,20 @@ module.exports = {
|
|||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
async getAll (_req, res) {
|
||||
const tags = await Tag.findAll({
|
||||
order: [[fn('COUNT', col('tag.tag')), 'DESC']],
|
||||
attributes: ['tag', [fn('COUNT', col('tag.tag')), 'count']],
|
||||
include: [{ model: Event, where: { is_visible: true }, attributes: [], through: { attributes: [] }, required: true }],
|
||||
group: ['tag.tag'],
|
||||
raw: true,
|
||||
})
|
||||
return res.json(tags)
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* search for tags by query string
|
||||
* sorted by usage
|
||||
|
@ -60,9 +76,48 @@ module.exports = {
|
|||
include: [{ model: Event, where: { is_visible: true }, attributes: [], through: { attributes: [] }, required: true }],
|
||||
group: ['tag.tag'],
|
||||
limit: 10,
|
||||
subQuery:false
|
||||
subQuery: false
|
||||
})
|
||||
|
||||
return res.json(tags.map(t => t.tag))
|
||||
},
|
||||
|
||||
async updateTag (req, res) {
|
||||
const tag = await Tag.findByPk(req.body.tag)
|
||||
await tag.update(req.body)
|
||||
res.json(place)
|
||||
},
|
||||
|
||||
async updateTag (req, res) {
|
||||
const oldtag = await Tag.findByPk(req.body.tag)
|
||||
const newtag = await Tag.findByPk(req.body.newTag)
|
||||
|
||||
// if the new tag does not exists, just rename the old one
|
||||
if (!newtag) {
|
||||
oldtag.tag = req.body.newTag
|
||||
await oldtag.update({ tag: req.body.newTag })
|
||||
} else {
|
||||
// in case it exists:
|
||||
// - search for events with old tag
|
||||
const events = await oldtag.getEvents()
|
||||
// - substitute it with the new one
|
||||
await oldtag.removeEvents(events)
|
||||
await newtag.addEvents(events)
|
||||
}
|
||||
res.sendStatus(200)
|
||||
},
|
||||
|
||||
async remove (req, res) {
|
||||
log.info('Remove tag', req.params.tag)
|
||||
const tagName = req.params.tag
|
||||
try {
|
||||
const tag = await Tag.findByPk(tagName)
|
||||
await tag.destroy()
|
||||
res.sendStatus(200)
|
||||
} catch (e) {
|
||||
log.error('Tag removal failed:', e)
|
||||
res.sendStatus(404)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -44,13 +44,13 @@ const userController = {
|
|||
},
|
||||
|
||||
async current (req, res) {
|
||||
if (!res.locals.user) { return res.status(400).send('Not logged') }
|
||||
const user = await User.scope('withoutPassword').findByPk(res.locals.user.id)
|
||||
if (!req.user) { return res.status(400).send('Not logged') }
|
||||
const user = await User.scope('withoutPassword').findByPk(req.user.id)
|
||||
res.json(user)
|
||||
},
|
||||
|
||||
async getAll (req, res) {
|
||||
const users = await User.scope(res.locals.user.is_admin ? 'withRecover' : 'withoutPassword').findAll({
|
||||
const users = await User.scope(req.user.is_admin ? 'withRecover' : 'withoutPassword').findAll({
|
||||
order: [['is_admin', 'DESC'], ['createdAt', 'DESC']]
|
||||
})
|
||||
res.json(users)
|
||||
|
@ -62,7 +62,7 @@ const userController = {
|
|||
|
||||
if (!user) { return res.status(404).json({ success: false, message: 'User not found!' }) }
|
||||
|
||||
if (req.body.id !== res.locals.user.id && !res.locals.user.is_admin) {
|
||||
if (req.body.id !== req.user.id && !req.user.is_admin) {
|
||||
return res.status(400).json({ succes: false, message: 'Not allowed' })
|
||||
}
|
||||
|
||||
|
@ -123,10 +123,10 @@ const userController = {
|
|||
async remove (req, res) {
|
||||
try {
|
||||
let user
|
||||
if (res.locals.user.is_admin && req.params.id) {
|
||||
if (req.user.is_admin && req.params.id) {
|
||||
user = await User.findByPk(req.params.id)
|
||||
} else {
|
||||
user = await User.findByPk(res.locals.user.id)
|
||||
user = await User.findByPk(req.user.id)
|
||||
}
|
||||
await user.destroy()
|
||||
log.warn(`User ${user.email} removed!`)
|
||||
|
|
|
@ -34,6 +34,7 @@ if (config.status !== 'READY') {
|
|||
const oauthController = require('./controller/oauth')
|
||||
const announceController = require('./controller/announce')
|
||||
const collectionController = require('./controller/collection')
|
||||
const pluginController = require('./controller/plugins')
|
||||
const helpers = require('../helpers')
|
||||
const storage = require('./storage')
|
||||
const upload = multer({ storage })
|
||||
|
@ -59,7 +60,7 @@ if (config.status !== 'READY') {
|
|||
```
|
||||
*/
|
||||
api.get('/ping', (_req, res) => res.sendStatus(200))
|
||||
api.get('/user', isAuth, (_req, res) => res.json(res.locals.user))
|
||||
api.get('/user', isAuth, (req, res) => res.json(req.user))
|
||||
|
||||
|
||||
api.post('/user/recover', userController.forgotPassword)
|
||||
|
@ -89,16 +90,16 @@ if (config.status !== 'READY') {
|
|||
* @param {integer} [end] - end timestamp (optional)
|
||||
* @param {array} [tags] - List of tags
|
||||
* @param {array} [places] - List of places id
|
||||
* @param {integer} [max] - Limit events
|
||||
* @param {integer} [max] - Limit events
|
||||
* @param {boolean} [show_recurrent] - Show also recurrent events (default: as choosen in admin settings)
|
||||
* @param {integer} [page] - Pagination
|
||||
* @param {boolean} [older] - select <= start instead of >=
|
||||
* @example ***Example***
|
||||
* [https://demo.gancio.org/api/events](https://demo.gancio.org/api/events)
|
||||
* @example ***Example***
|
||||
* [https://demo.gancio.org/api/events](https://demo.gancio.org/api/events)
|
||||
* [usage example](https://framagit.org/les/gancio/-/blob/master/webcomponents/src/GancioEvents.svelte#L18-42)
|
||||
*/
|
||||
|
||||
api.get('/events', cors, eventController.select)
|
||||
api.get('/events', cors, eventController.select)
|
||||
|
||||
/**
|
||||
* Add a new event
|
||||
|
@ -110,6 +111,8 @@ if (config.status !== 'READY') {
|
|||
* @param {string} description - event's description (html accepted and sanitized)
|
||||
* @param {string} place_name - the name of the place
|
||||
* @param {string} [place_address] - the address of the place
|
||||
* @param {float} [place_latitude] - the latitude of the place
|
||||
* @param {float} [place_longitude] - the longitude of the place
|
||||
* @param {integer} start_datetime - start timestamp
|
||||
* @param {integer} multidate - is a multidate event?
|
||||
* @param {array} tags - List of tags
|
||||
|
@ -140,6 +143,8 @@ if (config.status !== 'READY') {
|
|||
api.post('/settings', isAdmin, settingsController.setRequest)
|
||||
api.get('/settings', isAdmin, settingsController.getAll)
|
||||
api.post('/settings/logo', isAdmin, multer({ dest: config.upload_path }).single('logo'), settingsController.setLogo)
|
||||
api.post('/settings/fallbackImage', isAdmin, multer({ dest: config.upload_path }).single('fallbackImage'), settingsController.setFallbackImage)
|
||||
api.post('/settings/headerImage', isAdmin, multer({ dest: config.upload_path }).single('headerImage'), settingsController.setHeaderImage)
|
||||
api.post('/settings/smtp', isAdmin, settingsController.testSMTP)
|
||||
api.get('/settings/smtp', isAdmin, settingsController.getSMTPSettings)
|
||||
|
||||
|
@ -157,13 +162,21 @@ if (config.status !== 'READY') {
|
|||
api.get('/export/:type', cors, exportController.export)
|
||||
|
||||
|
||||
api.get('/place/all', isAdmin, placeController.getAll)
|
||||
// - PLACES
|
||||
api.get('/places', isAdmin, placeController.getAll)
|
||||
api.get('/place/:placeName', cors, placeController.getEvents)
|
||||
api.get('/place', cors, placeController.search)
|
||||
api.get('/placeOSM/Nominatim/:place_details', cors, placeController._nominatim)
|
||||
api.get('/placeOSM/Photon/:place_details', cors, placeController._photon)
|
||||
api.put('/place', isAdmin, placeController.updatePlace)
|
||||
|
||||
// - TAGS
|
||||
api.get('/tags', isAdmin, tagController.getAll)
|
||||
api.get('/tag', cors, tagController.search)
|
||||
api.get('/tag/:tag', cors, tagController.getEvents)
|
||||
api.delete('/tag/:tag', isAdmin, tagController.remove)
|
||||
api.put('/tag', isAdmin, tagController.updateTag)
|
||||
|
||||
|
||||
// - FEDIVERSE INSTANCES, MODERATION, RESOURCES
|
||||
api.get('/instances', isAdmin, instanceController.getAll)
|
||||
|
@ -189,6 +202,10 @@ if (config.status !== 'READY') {
|
|||
api.post('/filter', isAdmin, collectionController.addFilter)
|
||||
api.delete('/filter/:id', isAdmin, collectionController.removeFilter)
|
||||
|
||||
// - PLUGINS
|
||||
api.get('/plugins', isAdmin, pluginController.getAll)
|
||||
api.put('/plugin/:plugin', isAdmin, pluginController.togglePlugin)
|
||||
|
||||
// OAUTH
|
||||
api.get('/clients', isAuth, oauthController.getClients)
|
||||
api.get('/client/:client_id', isAuth, oauthController.getClient)
|
||||
|
|
|
@ -105,7 +105,9 @@ Event.prototype.toAP = function (username, locale, to = []) {
|
|||
endTime: this.end_datetime ? dayjs.unix(this.end_datetime).tz().locale(locale).format() : null,
|
||||
location: {
|
||||
name: this.place.name,
|
||||
address: this.place.address
|
||||
address: this.place.address,
|
||||
latitude: this.place.latitude,
|
||||
longitude: this.place.longitude
|
||||
},
|
||||
attachment,
|
||||
tag: tags && tags.map(tag => ({
|
||||
|
|
|
@ -10,7 +10,9 @@ Place.init({
|
|||
index: true,
|
||||
allowNull: false
|
||||
},
|
||||
address: DataTypes.STRING
|
||||
address: DataTypes.STRING,
|
||||
latitude: DataTypes.FLOAT,
|
||||
longitude: DataTypes.FLOAT,
|
||||
}, { sequelize, modelName: 'place' })
|
||||
|
||||
module.exports = Place
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
const express = require('express')
|
||||
const OAuthServer = require('express-oauth-server')
|
||||
const oauth = express.Router()
|
||||
const oauthController = require('./controller/oauth')
|
||||
const log = require('../log')
|
||||
|
||||
const oauthServer = new OAuthServer({
|
||||
model: oauthController.model,
|
||||
allowEmptyState: true,
|
||||
useErrorHandler: true,
|
||||
continueMiddleware: false,
|
||||
debug: true,
|
||||
requireClientAuthentication: { password: false },
|
||||
authenticateHandler: {
|
||||
handle (_req, res) {
|
||||
if (!res.locals.user) {
|
||||
throw new Error('Not authenticated!')
|
||||
}
|
||||
return res.locals.user
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
oauth.oauthServer = oauthServer
|
||||
oauth.use(express.json())
|
||||
oauth.use(express.urlencoded({ extended: false }))
|
||||
|
||||
oauth.post('/token', oauthServer.token())
|
||||
oauth.post('/login', oauthServer.token())
|
||||
|
||||
oauth.get('/authorize', oauthServer.authorize())
|
||||
|
||||
oauth.use((req, res) => res.sendStatus(404))
|
||||
|
||||
oauth.use((err, req, res, next) => {
|
||||
const error_msg = err.toString()
|
||||
log.warn('[OAUTH USE] ' + error_msg)
|
||||
res.status(500).send(error_msg)
|
||||
})
|
||||
|
||||
module.exports = oauth
|
|
@ -22,7 +22,6 @@ const { JSDOM } = require('jsdom')
|
|||
const { window } = new JSDOM('<!DOCTYPE html>')
|
||||
const domPurify = DOMPurify(window)
|
||||
const url = require('url')
|
||||
const locales = require('../locales')
|
||||
|
||||
domPurify.addHook('beforeSanitizeElements', node => {
|
||||
if (node.hasAttribute && node.hasAttribute('href')) {
|
||||
|
@ -49,7 +48,7 @@ domPurify.addHook('beforeSanitizeElements', node => {
|
|||
module.exports = {
|
||||
|
||||
randomString(length = 12) {
|
||||
const wishlist = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
||||
const wishlist = '0123456789abcdefghijklmnopqrstuvwxyz'
|
||||
return Array.from(crypto.randomFillSync(new Uint32Array(length)))
|
||||
.map(x => wishlist[x % wishlist.length])
|
||||
.join('')
|
||||
|
@ -63,26 +62,47 @@ module.exports = {
|
|||
})
|
||||
},
|
||||
|
||||
async setUserLocale(req, res, next) {
|
||||
// select locale based on cookie? and accept-language header
|
||||
acceptLanguage.languages(Object.keys(locales))
|
||||
res.locals.acceptedLocale = acceptLanguage.get(req.headers['accept-language'])
|
||||
dayjs.locale(res.locals.acceptedLocale)
|
||||
next()
|
||||
},
|
||||
|
||||
async initSettings(_req, res, next) {
|
||||
// initialize settings
|
||||
res.locals.settings = cloneDeep(settingsController.settings)
|
||||
delete res.locals.settings.smtp
|
||||
delete res.locals.settings.publicKey
|
||||
res.locals.settings.baseurl = config.baseurl
|
||||
res.locals.settings.hostname = config.hostname
|
||||
res.locals.settings.title = res.locals.settings.title || config.title
|
||||
res.locals.settings.description = res.locals.settings.description || config.description
|
||||
res.locals.settings.version = pkg.version
|
||||
// res.locals.settings = cloneDeep(settingsController.settings)
|
||||
const settings = settingsController.settings
|
||||
res.locals.settings = {
|
||||
title: settings.title || config.title,
|
||||
description: settings.description || config.description,
|
||||
baseurl: config.baseurl,
|
||||
hostname: config.hostname,
|
||||
version: pkg.version,
|
||||
instance_timezone: settings.instance_timezone,
|
||||
instance_locale: settings.instance_locale,
|
||||
instance_name: settings.instance_name,
|
||||
instance_place: settings.instance_place,
|
||||
allow_registration: settings.allow_registration,
|
||||
allow_anon_event: settings.allow_anon_event,
|
||||
allow_recurrent_event: settings.allow_recurrent_event,
|
||||
allow_multidate_event: settings.allow_multidate_event,
|
||||
recurrent_event_visible: settings.recurrent_event_visible,
|
||||
enable_federation: settings.enable_federation,
|
||||
enable_resources: settings.enable_resources,
|
||||
hide_boosts: settings.hide_boosts,
|
||||
enable_trusted_instances: settings.enable_trusted_instances,
|
||||
trusted_instances: settings.trusted_instances,
|
||||
trusted_instances_label: settings.trusted_instances_label,
|
||||
'theme.is_dark': settings['theme.is_dark'],
|
||||
'theme.primary': settings['theme.primary'],
|
||||
hide_thumbs: settings.hide_thumbs,
|
||||
hide_calendar: settings.hide_calendar,
|
||||
allow_geolocation: settings.allow_geolocation,
|
||||
geocoding_provider_type: settings.geocoding_provider_type,
|
||||
geocoding_provider: settings.geocoding_provider,
|
||||
geocoding_countrycodes: settings.geocoding_countrycodes,
|
||||
tilelayer_provider: settings.tilelayer_provider,
|
||||
tilelayer_provider_attribution: settings.tilelayer_provider_attribution,
|
||||
footerLinks: settings.footerLinks,
|
||||
about: settings.about
|
||||
}
|
||||
// set user locale
|
||||
res.locals.user_locale = settingsController.user_locale[res.locals.acceptedLocale]
|
||||
// res.locals.user_locale = settingsController.user_locale[res.locals.acceptedLocale]
|
||||
dayjs.tz.setDefault(res.locals.settings.instance_timezone)
|
||||
next()
|
||||
},
|
||||
|
@ -92,16 +112,30 @@ module.exports = {
|
|||
// serve images/thumb
|
||||
router.use('/media/', express.static(config.upload_path, { immutable: true, maxAge: '1y' }), (_req, res) => res.sendStatus(404))
|
||||
router.use('/download/:filename', (req, res) => {
|
||||
return res.download(req.params.filename, undefined, { root: config.upload_path }, err => {
|
||||
res.download(req.params.filename, undefined, { root: config.upload_path }, err => {
|
||||
if (err) {
|
||||
res.status(404).send('Not found (but nice try 😊)')
|
||||
// Check if headers have been sent
|
||||
if(res.headersSent) {
|
||||
log.warn(err)
|
||||
} else {
|
||||
res.status(404).send('Not found (but nice try 😊)')
|
||||
// }
|
||||
}
|
||||
})
|
||||
}})
|
||||
})
|
||||
|
||||
router.use('/fallbackimage.png', (req, res, next) => {
|
||||
const fallbackImagePath = settingsController.settings.fallback_image || './static/noimg.svg'
|
||||
return express.static(fallbackImagePath)(req, res, next)
|
||||
})
|
||||
|
||||
router.use('/headerimage.png', (req, res, next) => {
|
||||
const headerImagePath = settingsController.settings.header_image || './static/noimg.svg'
|
||||
return express.static(headerImagePath)(req, res, next)
|
||||
})
|
||||
router.use('/noimg.svg', express.static('./static/noimg.svg'))
|
||||
|
||||
router.use('/logo.png', (req, res, next) => {
|
||||
const logoPath = res.locals.settings.logo || './static/gancio'
|
||||
const logoPath = settingsController.settings.logo || './static/gancio'
|
||||
return express.static(logoPath + '.png')(req, res, next)
|
||||
})
|
||||
|
||||
|
|
987
server/helpers/geolocation.js
Normal file
987
server/helpers/geolocation.js
Normal file
|
@ -0,0 +1,987 @@
|
|||
// Iso conversions
|
||||
|
||||
var isoCountries = [
|
||||
{
|
||||
"code": "af",
|
||||
"name": "Afghanistan"
|
||||
},
|
||||
{
|
||||
"code": "ax",
|
||||
"name": "Aland Islands"
|
||||
},
|
||||
{
|
||||
"code": "al",
|
||||
"name": "Albania"
|
||||
},
|
||||
{
|
||||
"code": "dz",
|
||||
"name": "Algeria"
|
||||
},
|
||||
{
|
||||
"code": "as",
|
||||
"name": "American Samoa"
|
||||
},
|
||||
{
|
||||
"code": "ad",
|
||||
"name": "Andorra"
|
||||
},
|
||||
{
|
||||
"code": "ao",
|
||||
"name": "Angola"
|
||||
},
|
||||
{
|
||||
"code": "ai",
|
||||
"name": "Anguilla"
|
||||
},
|
||||
{
|
||||
"code": "aq",
|
||||
"name": "Antarctica"
|
||||
},
|
||||
{
|
||||
"code": "ag",
|
||||
"name": "Antigua And Barbuda"
|
||||
},
|
||||
{
|
||||
"code": "ar",
|
||||
"name": "Argentina"
|
||||
},
|
||||
{
|
||||
"code": "am",
|
||||
"name": "Armenia"
|
||||
},
|
||||
{
|
||||
"code": "aw",
|
||||
"name": "Aruba"
|
||||
},
|
||||
{
|
||||
"code": "au",
|
||||
"name": "Australia"
|
||||
},
|
||||
{
|
||||
"code": "at",
|
||||
"name": "Austria"
|
||||
},
|
||||
{
|
||||
"code": "az",
|
||||
"name": "Azerbaijan"
|
||||
},
|
||||
{
|
||||
"code": "bs",
|
||||
"name": "Bahamas"
|
||||
},
|
||||
{
|
||||
"code": "bh",
|
||||
"name": "Bahrain"
|
||||
},
|
||||
{
|
||||
"code": "bd",
|
||||
"name": "Bangladesh"
|
||||
},
|
||||
{
|
||||
"code": "bb",
|
||||
"name": "Barbados"
|
||||
},
|
||||
{
|
||||
"code": "by",
|
||||
"name": "Belarus"
|
||||
},
|
||||
{
|
||||
"code": "be",
|
||||
"name": "Belgium"
|
||||
},
|
||||
{
|
||||
"code": "bz",
|
||||
"name": "Belize"
|
||||
},
|
||||
{
|
||||
"code": "bj",
|
||||
"name": "Benin"
|
||||
},
|
||||
{
|
||||
"code": "bm",
|
||||
"name": "Bermuda"
|
||||
},
|
||||
{
|
||||
"code": "bt",
|
||||
"name": "Bhutan"
|
||||
},
|
||||
{
|
||||
"code": "bo",
|
||||
"name": "Bolivia"
|
||||
},
|
||||
{
|
||||
"code": "ba",
|
||||
"name": "Bosnia And Herzegovina"
|
||||
},
|
||||
{
|
||||
"code": "bw",
|
||||
"name": "Botswana"
|
||||
},
|
||||
{
|
||||
"code": "bv",
|
||||
"name": "Bouvet Island"
|
||||
},
|
||||
{
|
||||
"code": "br",
|
||||
"name": "Brazil"
|
||||
},
|
||||
{
|
||||
"code": "io",
|
||||
"name": "British Indian Ocean Territory"
|
||||
},
|
||||
{
|
||||
"code": "bn",
|
||||
"name": "Brunei Darussalam"
|
||||
},
|
||||
{
|
||||
"code": "bg",
|
||||
"name": "Bulgaria"
|
||||
},
|
||||
{
|
||||
"code": "bf",
|
||||
"name": "Burkina Faso"
|
||||
},
|
||||
{
|
||||
"code": "bi",
|
||||
"name": "Burundi"
|
||||
},
|
||||
{
|
||||
"code": "kh",
|
||||
"name": "Cambodia"
|
||||
},
|
||||
{
|
||||
"code": "cm",
|
||||
"name": "Cameroon"
|
||||
},
|
||||
{
|
||||
"code": "ca",
|
||||
"name": "Canada"
|
||||
},
|
||||
{
|
||||
"code": "cv",
|
||||
"name": "Cape Verde"
|
||||
},
|
||||
{
|
||||
"code": "ky",
|
||||
"name": "Cayman Islands"
|
||||
},
|
||||
{
|
||||
"code": "cf",
|
||||
"name": "Central African Republic"
|
||||
},
|
||||
{
|
||||
"code": "td",
|
||||
"name": "Chad"
|
||||
},
|
||||
{
|
||||
"code": "cl",
|
||||
"name": "Chile"
|
||||
},
|
||||
{
|
||||
"code": "cn",
|
||||
"name": "China"
|
||||
},
|
||||
{
|
||||
"code": "cx",
|
||||
"name": "Christmas Island"
|
||||
},
|
||||
{
|
||||
"code": "cc",
|
||||
"name": "Cocos (Keeling) Islands"
|
||||
},
|
||||
{
|
||||
"code": "co",
|
||||
"name": "Colombia"
|
||||
},
|
||||
{
|
||||
"code": "km",
|
||||
"name": "Comoros"
|
||||
},
|
||||
{
|
||||
"code": "cg",
|
||||
"name": "Congo"
|
||||
},
|
||||
{
|
||||
"code": "cd",
|
||||
"name": "Congo, Democratic Republic"
|
||||
},
|
||||
{
|
||||
"code": "ck",
|
||||
"name": "Cook Islands"
|
||||
},
|
||||
{
|
||||
"code": "cr",
|
||||
"name": "Costa Rica"
|
||||
},
|
||||
{
|
||||
"code": "ci",
|
||||
"name": "Cote D'Ivoire"
|
||||
},
|
||||
{
|
||||
"code": "hr",
|
||||
"name": "Croatia"
|
||||
},
|
||||
{
|
||||
"code": "cu",
|
||||
"name": "Cuba"
|
||||
},
|
||||
{
|
||||
"code": "cy",
|
||||
"name": "Cyprus"
|
||||
},
|
||||
{
|
||||
"code": "cz",
|
||||
"name": "Czech Republic"
|
||||
},
|
||||
{
|
||||
"code": "dk",
|
||||
"name": "Denmark"
|
||||
},
|
||||
{
|
||||
"code": "dj",
|
||||
"name": "Djibouti"
|
||||
},
|
||||
{
|
||||
"code": "dm",
|
||||
"name": "Dominica"
|
||||
},
|
||||
{
|
||||
"code": "do",
|
||||
"name": "Dominican Republic"
|
||||
},
|
||||
{
|
||||
"code": "ec",
|
||||
"name": "Ecuador"
|
||||
},
|
||||
{
|
||||
"code": "eg",
|
||||
"name": "Egypt"
|
||||
},
|
||||
{
|
||||
"code": "sv",
|
||||
"name": "El Salvador"
|
||||
},
|
||||
{
|
||||
"code": "gq",
|
||||
"name": "Equatorial Guinea"
|
||||
},
|
||||
{
|
||||
"code": "er",
|
||||
"name": "Eritrea"
|
||||
},
|
||||
{
|
||||
"code": "ee",
|
||||
"name": "Estonia"
|
||||
},
|
||||
{
|
||||
"code": "et",
|
||||
"name": "Ethiopia"
|
||||
},
|
||||
{
|
||||
"code": "fk",
|
||||
"name": "Falkland Islands (Malvinas)"
|
||||
},
|
||||
{
|
||||
"code": "fo",
|
||||
"name": "Faroe Islands"
|
||||
},
|
||||
{
|
||||
"code": "fj",
|
||||
"name": "Fiji"
|
||||
},
|
||||
{
|
||||
"code": "fi",
|
||||
"name": "Finland"
|
||||
},
|
||||
{
|
||||
"code": "fr",
|
||||
"name": "France"
|
||||
},
|
||||
{
|
||||
"code": "gf",
|
||||
"name": "French Guiana"
|
||||
},
|
||||
{
|
||||
"code": "pf",
|
||||
"name": "French Polynesia"
|
||||
},
|
||||
{
|
||||
"code": "tf",
|
||||
"name": "French Southern Territories"
|
||||
},
|
||||
{
|
||||
"code": "ga",
|
||||
"name": "Gabon"
|
||||
},
|
||||
{
|
||||
"code": "gm",
|
||||
"name": "Gambia"
|
||||
},
|
||||
{
|
||||
"code": "ge",
|
||||
"name": "Georgia"
|
||||
},
|
||||
{
|
||||
"code": "de",
|
||||
"name": "Germany"
|
||||
},
|
||||
{
|
||||
"code": "gh",
|
||||
"name": "Ghana"
|
||||
},
|
||||
{
|
||||
"code": "gi",
|
||||
"name": "Gibraltar"
|
||||
},
|
||||
{
|
||||
"code": "gr",
|
||||
"name": "Greece"
|
||||
},
|
||||
{
|
||||
"code": "gl",
|
||||
"name": "Greenland"
|
||||
},
|
||||
{
|
||||
"code": "gd",
|
||||
"name": "Grenada"
|
||||
},
|
||||
{
|
||||
"code": "gp",
|
||||
"name": "Guadeloupe"
|
||||
},
|
||||
{
|
||||
"code": "gu",
|
||||
"name": "Guam"
|
||||
},
|
||||
{
|
||||
"code": "gt",
|
||||
"name": "Guatemala"
|
||||
},
|
||||
{
|
||||
"code": "gg",
|
||||
"name": "Guernsey"
|
||||
},
|
||||
{
|
||||
"code": "gn",
|
||||
"name": "Guinea"
|
||||
},
|
||||
{
|
||||
"code": "gw",
|
||||
"name": "Guinea-Bissau"
|
||||
},
|
||||
{
|
||||
"code": "gy",
|
||||
"name": "Guyana"
|
||||
},
|
||||
{
|
||||
"code": "ht",
|
||||
"name": "Haiti"
|
||||
},
|
||||
{
|
||||
"code": "hm",
|
||||
"name": "Heard Island & Mcdonald Islands"
|
||||
},
|
||||
{
|
||||
"code": "va",
|
||||
"name": "Holy See (Vatican City State)"
|
||||
},
|
||||
{
|
||||
"code": "hn",
|
||||
"name": "Honduras"
|
||||
},
|
||||
{
|
||||
"code": "hk",
|
||||
"name": "Hong Kong"
|
||||
},
|
||||
{
|
||||
"code": "hu",
|
||||
"name": "Hungary"
|
||||
},
|
||||
{
|
||||
"code": "is",
|
||||
"name": "Iceland"
|
||||
},
|
||||
{
|
||||
"code": "in",
|
||||
"name": "India"
|
||||
},
|
||||
{
|
||||
"code": "id",
|
||||
"name": "Indonesia"
|
||||
},
|
||||
{
|
||||
"code": "ir",
|
||||
"name": "Iran, Islamic Republic Of"
|
||||
},
|
||||
{
|
||||
"code": "iq",
|
||||
"name": "Iraq"
|
||||
},
|
||||
{
|
||||
"code": "ie",
|
||||
"name": "Ireland"
|
||||
},
|
||||
{
|
||||
"code": "im",
|
||||
"name": "Isle Of Man"
|
||||
},
|
||||
{
|
||||
"code": "il",
|
||||
"name": "Israel"
|
||||
},
|
||||
{
|
||||
"code": "it",
|
||||
"name": "Italy"
|
||||
},
|
||||
{
|
||||
"code": "jm",
|
||||
"name": "Jamaica"
|
||||
},
|
||||
{
|
||||
"code": "jp",
|
||||
"name": "Japan"
|
||||
},
|
||||
{
|
||||
"code": "je",
|
||||
"name": "Jersey"
|
||||
},
|
||||
{
|
||||
"code": "jo",
|
||||
"name": "Jordan"
|
||||
},
|
||||
{
|
||||
"code": "kz",
|
||||
"name": "Kazakhstan"
|
||||
},
|
||||
{
|
||||
"code": "ke",
|
||||
"name": "Kenya"
|
||||
},
|
||||
{
|
||||
"code": "ki",
|
||||
"name": "Kiribati"
|
||||
},
|
||||
{
|
||||
"code": "kr",
|
||||
"name": "Korea"
|
||||
},
|
||||
{
|
||||
"code": "kw",
|
||||
"name": "Kuwait"
|
||||
},
|
||||
{
|
||||
"code": "kg",
|
||||
"name": "Kyrgyzstan"
|
||||
},
|
||||
{
|
||||
"code": "la",
|
||||
"name": "Lao People's Democratic Republic"
|
||||
},
|
||||
{
|
||||
"code": "lv",
|
||||
"name": "Latvia"
|
||||
},
|
||||
{
|
||||
"code": "lb",
|
||||
"name": "Lebanon"
|
||||
},
|
||||
{
|
||||
"code": "ls",
|
||||
"name": "Lesotho"
|
||||
},
|
||||
{
|
||||
"code": "lr",
|
||||
"name": "Liberia"
|
||||
},
|
||||
{
|
||||
"code": "ly",
|
||||
"name": "Libyan Arab Jamahiriya"
|
||||
},
|
||||
{
|
||||
"code": "li",
|
||||
"name": "Liechtenstein"
|
||||
},
|
||||
{
|
||||
"code": "lt",
|
||||
"name": "Lithuania"
|
||||
},
|
||||
{
|
||||
"code": "lu",
|
||||
"name": "Luxembourg"
|
||||
},
|
||||
{
|
||||
"code": "mo",
|
||||
"name": "Macao"
|
||||
},
|
||||
{
|
||||
"code": "mk",
|
||||
"name": "Macedonia"
|
||||
},
|
||||
{
|
||||
"code": "mg",
|
||||
"name": "Madagascar"
|
||||
},
|
||||
{
|
||||
"code": "mw",
|
||||
"name": "Malawi"
|
||||
},
|
||||
{
|
||||
"code": "my",
|
||||
"name": "Malaysia"
|
||||
},
|
||||
{
|
||||
"code": "mv",
|
||||
"name": "Maldives"
|
||||
},
|
||||
{
|
||||
"code": "ml",
|
||||
"name": "Mali"
|
||||
},
|
||||
{
|
||||
"code": "mt",
|
||||
"name": "Malta"
|
||||
},
|
||||
{
|
||||
"code": "mh",
|
||||
"name": "Marshall Islands"
|
||||
},
|
||||
{
|
||||
"code": "mq",
|
||||
"name": "Martinique"
|
||||
},
|
||||
{
|
||||
"code": "mr",
|
||||
"name": "Mauritania"
|
||||
},
|
||||
{
|
||||
"code": "mu",
|
||||
"name": "Mauritius"
|
||||
},
|
||||
{
|
||||
"code": "yt",
|
||||
"name": "Mayotte"
|
||||
},
|
||||
{
|
||||
"code": "mx",
|
||||
"name": "Mexico"
|
||||
},
|
||||
{
|
||||
"code": "fm",
|
||||
"name": "Micronesia, Federated States Of"
|
||||
},
|
||||
{
|
||||
"code": "md",
|
||||
"name": "Moldova"
|
||||
},
|
||||
{
|
||||
"code": "mc",
|
||||
"name": "Monaco"
|
||||
},
|
||||
{
|
||||
"code": "mn",
|
||||
"name": "Mongolia"
|
||||
},
|
||||
{
|
||||
"code": "me",
|
||||
"name": "Montenegro"
|
||||
},
|
||||
{
|
||||
"code": "ms",
|
||||
"name": "Montserrat"
|
||||
},
|
||||
{
|
||||
"code": "ma",
|
||||
"name": "Morocco"
|
||||
},
|
||||
{
|
||||
"code": "mz",
|
||||
"name": "Mozambique"
|
||||
},
|
||||
{
|
||||
"code": "mm",
|
||||
"name": "Myanmar"
|
||||
},
|
||||
{
|
||||
"code": "na",
|
||||
"name": "Namibia"
|
||||
},
|
||||
{
|
||||
"code": "nr",
|
||||
"name": "Nauru"
|
||||
},
|
||||
{
|
||||
"code": "np",
|
||||
"name": "Nepal"
|
||||
},
|
||||
{
|
||||
"code": "nl",
|
||||
"name": "Netherlands"
|
||||
},
|
||||
{
|
||||
"code": "an",
|
||||
"name": "Netherlands Antilles"
|
||||
},
|
||||
{
|
||||
"code": "nc",
|
||||
"name": "New Caledonia"
|
||||
},
|
||||
{
|
||||
"code": "nz",
|
||||
"name": "New Zealand"
|
||||
},
|
||||
{
|
||||
"code": "ni",
|
||||
"name": "Nicaragua"
|
||||
},
|
||||
{
|
||||
"code": "ne",
|
||||
"name": "Niger"
|
||||
},
|
||||
{
|
||||
"code": "ng",
|
||||
"name": "Nigeria"
|
||||
},
|
||||
{
|
||||
"code": "nu",
|
||||
"name": "Niue"
|
||||
},
|
||||
{
|
||||
"code": "nf",
|
||||
"name": "Norfolk Island"
|
||||
},
|
||||
{
|
||||
"code": "mp",
|
||||
"name": "Northern Mariana Islands"
|
||||
},
|
||||
{
|
||||
"code": "no",
|
||||
"name": "Norway"
|
||||
},
|
||||
{
|
||||
"code": "om",
|
||||
"name": "Oman"
|
||||
},
|
||||
{
|
||||
"code": "pk",
|
||||
"name": "Pakistan"
|
||||
},
|
||||
{
|
||||
"code": "pw",
|
||||
"name": "Palau"
|
||||
},
|
||||
{
|
||||
"code": "ps",
|
||||
"name": "Palestinian Territory, Occupied"
|
||||
},
|
||||
{
|
||||
"code": "pa",
|
||||
"name": "Panama"
|
||||
},
|
||||
{
|
||||
"code": "pg",
|
||||
"name": "Papua New Guinea"
|
||||
},
|
||||
{
|
||||
"code": "py",
|
||||
"name": "Paraguay"
|
||||
},
|
||||
{
|
||||
"code": "pe",
|
||||
"name": "Peru"
|
||||
},
|
||||
{
|
||||
"code": "ph",
|
||||
"name": "Philippines"
|
||||
},
|
||||
{
|
||||
"code": "pn",
|
||||
"name": "Pitcairn"
|
||||
},
|
||||
{
|
||||
"code": "pl",
|
||||
"name": "Poland"
|
||||
},
|
||||
{
|
||||
"code": "pt",
|
||||
"name": "Portugal"
|
||||
},
|
||||
{
|
||||
"code": "pr",
|
||||
"name": "Puerto Rico"
|
||||
},
|
||||
{
|
||||
"code": "qa",
|
||||
"name": "Qatar"
|
||||
},
|
||||
{
|
||||
"code": "re",
|
||||
"name": "Reunion"
|
||||
},
|
||||
{
|
||||
"code": "ro",
|
||||
"name": "Romania"
|
||||
},
|
||||
{
|
||||
"code": "ru",
|
||||
"name": "Russian Federation"
|
||||
},
|
||||
{
|
||||
"code": "rw",
|
||||
"name": "Rwanda"
|
||||
},
|
||||
{
|
||||
"code": "bl",
|
||||
"name": "Saint Barthelemy"
|
||||
},
|
||||
{
|
||||
"code": "sh",
|
||||
"name": "Saint Helena"
|
||||
},
|
||||
{
|
||||
"code": "kn",
|
||||
"name": "Saint Kitts And Nevis"
|
||||
},
|
||||
{
|
||||
"code": "lc",
|
||||
"name": "Saint Lucia"
|
||||
},
|
||||
{
|
||||
"code": "mf",
|
||||
"name": "Saint Martin"
|
||||
},
|
||||
{
|
||||
"code": "pm",
|
||||
"name": "Saint Pierre And Miquelon"
|
||||
},
|
||||
{
|
||||
"code": "vc",
|
||||
"name": "Saint Vincent And Grenadines"
|
||||
},
|
||||
{
|
||||
"code": "ws",
|
||||
"name": "Samoa"
|
||||
},
|
||||
{
|
||||
"code": "sm",
|
||||
"name": "San Marino"
|
||||
},
|
||||
{
|
||||
"code": "st",
|
||||
"name": "Sao Tome And Principe"
|
||||
},
|
||||
{
|
||||
"code": "sa",
|
||||
"name": "Saudi Arabia"
|
||||
},
|
||||
{
|
||||
"code": "sn",
|
||||
"name": "Senegal"
|
||||
},
|
||||
{
|
||||
"code": "rs",
|
||||
"name": "Serbia"
|
||||
},
|
||||
{
|
||||
"code": "sc",
|
||||
"name": "Seychelles"
|
||||
},
|
||||
{
|
||||
"code": "sl",
|
||||
"name": "Sierra Leone"
|
||||
},
|
||||
{
|
||||
"code": "sg",
|
||||
"name": "Singapore"
|
||||
},
|
||||
{
|
||||
"code": "sk",
|
||||
"name": "Slovakia"
|
||||
},
|
||||
{
|
||||
"code": "si",
|
||||
"name": "Slovenia"
|
||||
},
|
||||
{
|
||||
"code": "sb",
|
||||
"name": "Solomon Islands"
|
||||
},
|
||||
{
|
||||
"code": "so",
|
||||
"name": "Somalia"
|
||||
},
|
||||
{
|
||||
"code": "za",
|
||||
"name": "South Africa"
|
||||
},
|
||||
{
|
||||
"code": "gs",
|
||||
"name": "South Georgia And Sandwich Isl."
|
||||
},
|
||||
{
|
||||
"code": "es",
|
||||
"name": "Spain"
|
||||
},
|
||||
{
|
||||
"code": "lk",
|
||||
"name": "Sri Lanka"
|
||||
},
|
||||
{
|
||||
"code": "sd",
|
||||
"name": "Sudan"
|
||||
},
|
||||
{
|
||||
"code": "sr",
|
||||
"name": "Suriname"
|
||||
},
|
||||
{
|
||||
"code": "sj",
|
||||
"name": "Svalbard And Jan Mayen"
|
||||
},
|
||||
{
|
||||
"code": "sz",
|
||||
"name": "Swaziland"
|
||||
},
|
||||
{
|
||||
"code": "se",
|
||||
"name": "Sweden"
|
||||
},
|
||||
{
|
||||
"code": "ch",
|
||||
"name": "Switzerland"
|
||||
},
|
||||
{
|
||||
"code": "sy",
|
||||
"name": "Syrian Arab Republic"
|
||||
},
|
||||
{
|
||||
"code": "tw",
|
||||
"name": "Taiwan"
|
||||
},
|
||||
{
|
||||
"code": "tj",
|
||||
"name": "Tajikistan"
|
||||
},
|
||||
{
|
||||
"code": "tz",
|
||||
"name": "Tanzania"
|
||||
},
|
||||
{
|
||||
"code": "th",
|
||||
"name": "Thailand"
|
||||
},
|
||||
{
|
||||
"code": "tl",
|
||||
"name": "Timor-Leste"
|
||||
},
|
||||
{
|
||||
"code": "tg",
|
||||
"name": "Togo"
|
||||
},
|
||||
{
|
||||
"code": "tk",
|
||||
"name": "Tokelau"
|
||||
},
|
||||
{
|
||||
"code": "to",
|
||||
"name": "Tonga"
|
||||
},
|
||||
{
|
||||
"code": "tt",
|
||||
"name": "Trinidad And Tobago"
|
||||
},
|
||||
{
|
||||
"code": "tn",
|
||||
"name": "Tunisia"
|
||||
},
|
||||
{
|
||||
"code": "tr",
|
||||
"name": "Turkey"
|
||||
},
|
||||
{
|
||||
"code": "tm",
|
||||
"name": "Turkmenistan"
|
||||
},
|
||||
{
|
||||
"code": "tc",
|
||||
"name": "Turks And Caicos Islands"
|
||||
},
|
||||
{
|
||||
"code": "tv",
|
||||
"name": "Tuvalu"
|
||||
},
|
||||
{
|
||||
"code": "ug",
|
||||
"name": "Uganda"
|
||||
},
|
||||
{
|
||||
"code": "ua",
|
||||
"name": "Ukraine"
|
||||
},
|
||||
{
|
||||
"code": "ae",
|
||||
"name": "United Arab Emirates"
|
||||
},
|
||||
{
|
||||
"code": "gb",
|
||||
"name": "United Kingdom"
|
||||
},
|
||||
{
|
||||
"code": "us",
|
||||
"name": "United States"
|
||||
},
|
||||
{
|
||||
"code": "um",
|
||||
"name": "United States Outlying Islands"
|
||||
},
|
||||
{
|
||||
"code": "uy",
|
||||
"name": "Uruguay"
|
||||
},
|
||||
{
|
||||
"code": "uz",
|
||||
"name": "Uzbekistan"
|
||||
},
|
||||
{
|
||||
"code": "vu",
|
||||
"name": "Vanuatu"
|
||||
},
|
||||
{
|
||||
"code": "ve",
|
||||
"name": "Venezuela"
|
||||
},
|
||||
{
|
||||
"code": "vn",
|
||||
"name": "Viet Nam"
|
||||
},
|
||||
{
|
||||
"code": "vg",
|
||||
"name": "Virgin Islands, British"
|
||||
},
|
||||
{
|
||||
"code": "vi",
|
||||
"name": "Virgin Islands, U.S."
|
||||
},
|
||||
{
|
||||
"code": "wf",
|
||||
"name": "Wallis And Futuna"
|
||||
},
|
||||
{
|
||||
"code": "eh",
|
||||
"name": "Western Sahara"
|
||||
},
|
||||
{
|
||||
"code": "ye",
|
||||
"name": "Yemen"
|
||||
},
|
||||
{
|
||||
"code": "zm",
|
||||
"name": "Zambia"
|
||||
},
|
||||
{
|
||||
"code": "zw",
|
||||
"name": "Zimbabwe"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
module.exports = { isoCountries }
|
31
server/migrations/20220706090946-place-details.js
Normal file
31
server/migrations/20220706090946-place-details.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up (queryInterface, Sequelize) {
|
||||
/**
|
||||
* Add altering commands here.
|
||||
*
|
||||
* Example:
|
||||
* await queryInterface.createTable('users', { id: Sequelize.INTEGER });
|
||||
*/
|
||||
return Promise.all(
|
||||
[
|
||||
await queryInterface.addColumn('places', 'latitude', { type: Sequelize.FLOAT }),
|
||||
await queryInterface.addColumn('places', 'longitude', { type: Sequelize.FLOAT })
|
||||
])
|
||||
},
|
||||
|
||||
async down (queryInterface, Sequelize) {
|
||||
/**
|
||||
* Add reverting commands here.
|
||||
*
|
||||
* Example:
|
||||
* await queryInterface.dropTable('users');
|
||||
*/
|
||||
return Promise.all(
|
||||
[
|
||||
await queryInterface.removeColumn('places', 'latitude'),
|
||||
await queryInterface.removeColumn('places', 'longitude')
|
||||
])
|
||||
}
|
||||
};
|
|
@ -1,15 +1,15 @@
|
|||
const express = require('express')
|
||||
const cookieParser = require('cookie-parser')
|
||||
const app = express()
|
||||
const initialize = require('./initialize.server')
|
||||
|
||||
const config = require('./config')
|
||||
const helpers = require('./helpers')
|
||||
app.use(helpers.setUserLocale)
|
||||
app.use(helpers.initSettings)
|
||||
app.use(helpers.logRequest)
|
||||
app.use(helpers.serveStatic())
|
||||
app.use(cookieParser())
|
||||
|
||||
app.use([
|
||||
helpers.initSettings,
|
||||
helpers.logRequest,
|
||||
helpers.serveStatic()
|
||||
])
|
||||
|
||||
async function main () {
|
||||
|
||||
|
@ -19,7 +19,6 @@ async function main () {
|
|||
// const promBundle = require('express-prom-bundle')
|
||||
// const metricsMiddleware = promBundle({ includeMethod: true })
|
||||
|
||||
|
||||
const log = require('./log')
|
||||
const api = require('./api')
|
||||
|
||||
|
@ -29,14 +28,13 @@ async function main () {
|
|||
if (config.status === 'READY') {
|
||||
const cors = require('cors')
|
||||
const { spamFilter } = require('./federation/helpers')
|
||||
const oauth = require('./api/oauth')
|
||||
const auth = require('./api/auth')
|
||||
const federation = require('./federation')
|
||||
const webfinger = require('./federation/webfinger')
|
||||
const exportController = require('./api/controller/export')
|
||||
const tagController = require('./api/controller/tag')
|
||||
const placeController = require('./api/controller/place')
|
||||
const collectionController = require('./api/controller/collection')
|
||||
const authController = require('./api/controller/oauth')
|
||||
|
||||
// rss / ics feed
|
||||
app.use(helpers.feedRedirect)
|
||||
|
@ -44,7 +42,6 @@ async function main () {
|
|||
app.get('/feed/:format/place/:placeName', cors(), placeController.getEvents)
|
||||
app.get('/feed/:format/collection/:name', cors(), collectionController.getEvents)
|
||||
app.get('/feed/:format', cors(), exportController.export)
|
||||
|
||||
|
||||
app.use('/event/:slug', helpers.APRedirect)
|
||||
|
||||
|
@ -55,11 +52,11 @@ async function main () {
|
|||
// ignore unimplemented ping url from fediverse
|
||||
app.use(spamFilter)
|
||||
|
||||
// fill res.locals.user if request is authenticated
|
||||
app.use(auth.fillUser)
|
||||
|
||||
app.use('/oauth', oauth)
|
||||
// app.use(metricsMiddleware)
|
||||
app.use(authController.authenticate)
|
||||
app.post('/oauth/token', authController.token)
|
||||
app.post('/oauth/login', authController.login)
|
||||
app.get('/oauth/authorize', authController.authorization)
|
||||
app.post('/oauth/authorize', authController.decision)
|
||||
}
|
||||
|
||||
// api!
|
||||
|
@ -73,10 +70,9 @@ async function main () {
|
|||
|
||||
// remaining request goes to nuxt
|
||||
// first nuxt component is ./pages/index.vue (with ./layouts/default.vue)
|
||||
// prefill current events, tags, places and announcements (used in every path)
|
||||
app.use(async (_req, res, next) => {
|
||||
if (config.status === 'READY') {
|
||||
|
||||
// TODO: fetch into layout!
|
||||
const announceController = require('./api/controller/announce')
|
||||
res.locals.announcements = await announceController._getVisible()
|
||||
}
|
||||
|
@ -91,6 +87,8 @@ if (process.env.NODE_ENV !== 'test') {
|
|||
main()
|
||||
}
|
||||
|
||||
// app.listen(13120)
|
||||
|
||||
module.exports = {
|
||||
main,
|
||||
handler: app,
|
||||
|
|
|
@ -313,13 +313,13 @@ function Le(t) {
|
|||
let e, i, n;
|
||||
return {
|
||||
c() {
|
||||
e = g("img"), a(e, "style", "aspect-ratio=1.7778;"), a(e, "alt", i = t[12].title), G(e.src, n = t[0] + "/noimg.svg") || a(e, "src", n), a(e, "loading", "lazy");
|
||||
e = g("img"), a(e, "style", "aspect-ratio=1.7778;"), a(e, "alt", i = t[12].title), G(e.src, n = t[0] + "/fallbackimage.png") || a(e, "src", n), a(e, "loading", "lazy");
|
||||
},
|
||||
m(l, o) {
|
||||
v(l, e, o);
|
||||
},
|
||||
p(l, o) {
|
||||
o & 32 && i !== (i = l[12].title) && a(e, "alt", i), o & 1 && !G(e.src, n = l[0] + "/noimg.svg") && a(e, "src", n);
|
||||
o & 32 && i !== (i = l[12].title) && a(e, "alt", i), o & 1 && !G(e.src, n = l[0] + "/fallbackimage.png") && a(e, "src", n);
|
||||
},
|
||||
d(l) {
|
||||
l && x(e);
|
||||
|
@ -454,7 +454,7 @@ function Re(t, e, i) {
|
|||
}), t.$$set = (d) => {
|
||||
"baseurl" in d && i(0, n = d.baseurl), "title" in d && i(1, l = d.title), "maxlength" in d && i(6, o = d.maxlength), "tags" in d && i(7, r = d.tags), "places" in d && i(8, f = d.places), "theme" in d && i(2, c = d.theme), "show_recurrent" in d && i(9, s = d.show_recurrent), "sidebar" in d && i(3, k = d.sidebar), "external_style" in d && i(4, m = d.external_style);
|
||||
}, t.$$.update = () => {
|
||||
t.$$.dirty & 974 && w();
|
||||
t.$$.dirty & 975 && w();
|
||||
}, [
|
||||
n,
|
||||
l,
|
||||
|
|
|
@ -1,21 +1,30 @@
|
|||
import dayjs from 'dayjs'
|
||||
|
||||
export const state = () => ({
|
||||
locale: '',
|
||||
user_locale: {},
|
||||
settings: {
|
||||
instance_timezone: 'Europe/Rome',
|
||||
instance_name: '',
|
||||
allow_registration: true,
|
||||
allow_anon_event: true,
|
||||
allow_multidate_event: true,
|
||||
allow_recurrent_event: true,
|
||||
recurrent_event_visible: false,
|
||||
allow_geolocation: false,
|
||||
geocoding_provider_type: '',
|
||||
geocoding_provider: '',
|
||||
geocoding_countrycodes: [],
|
||||
tilelayer_provider: '',
|
||||
tilelayer_provider_attribution: '',
|
||||
enable_federation: false,
|
||||
enable_resources: false,
|
||||
hide_boosts: true,
|
||||
enable_trusted_instances: true,
|
||||
trusted_instances: [],
|
||||
trusted_instances_label: '',
|
||||
footerLinks: []
|
||||
},
|
||||
announcements: []
|
||||
announcements: [],
|
||||
events: []
|
||||
})
|
||||
|
||||
export const mutations = {
|
||||
|
@ -25,14 +34,11 @@ export const mutations = {
|
|||
setSetting (state, setting) {
|
||||
state.settings[setting.key] = setting.value
|
||||
},
|
||||
setLocale (state, locale) {
|
||||
state.locale = locale
|
||||
},
|
||||
setUserlocale (state, messages) {
|
||||
state.user_locale = messages
|
||||
},
|
||||
setAnnouncements (state, announcements) {
|
||||
state.announcements = announcements
|
||||
},
|
||||
setEvents (state, events) {
|
||||
state.events = events
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,5 +61,14 @@ export const actions = {
|
|||
async setSetting ({ commit }, setting) {
|
||||
await this.$axios.$post('/settings', setting)
|
||||
commit('setSetting', setting)
|
||||
},
|
||||
async getEvents ({ commit, state }, params = {}) {
|
||||
const events = await this.$api.getEvents({
|
||||
start: params.start || dayjs().startOf('month').unix(),
|
||||
end: params.end || null,
|
||||
show_recurrent: params.show_recurrent || state.settings.recurrent_event_visible
|
||||
})
|
||||
commit('setEvents', events)
|
||||
return events
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ let app
|
|||
let places = []
|
||||
|
||||
beforeAll(async () => {
|
||||
|
||||
switch (process.env.DB) {
|
||||
case 'mariadb':
|
||||
process.env.config_path = path.resolve(__dirname, './seeds/config.mariadb.json')
|
||||
|
@ -54,7 +55,8 @@ describe('Authentication / Authorization', () => {
|
|||
test('should not authenticate with wrong user/password', () => {
|
||||
return request(app).post('/oauth/login')
|
||||
.set('Content-Type', 'application/x-www-form-urlencoded')
|
||||
.expect(500)
|
||||
.send({ email: 'admin', password: 'wrong'})
|
||||
.expect(401)
|
||||
})
|
||||
|
||||
test('should register an admin as first user', async () => {
|
||||
|
@ -155,7 +157,7 @@ describe('Events', () => {
|
|||
const required_fields = {
|
||||
'title': {},
|
||||
'start_datetime': { title: 'test title' },
|
||||
'place_id or place_name and place_address': { title: 'test title', start_datetime: dayjs().unix() + 1000, place_name: 'test place name' },
|
||||
'place_id or place_name and place_address are': { title: 'test title', start_datetime: dayjs().unix() + 1000, place_name: 'test place name' },
|
||||
}
|
||||
|
||||
const promises = Object.keys(required_fields).map(async field => {
|
||||
|
@ -200,9 +202,9 @@ describe('Events', () => {
|
|||
|
||||
})
|
||||
|
||||
test('should trim tags', async () => {
|
||||
test('should trim tags and title', async () => {
|
||||
const event = {
|
||||
title: 'test title 4',
|
||||
title: ' test title 4 ',
|
||||
place_id: places[0],
|
||||
start_datetime: dayjs().unix() + 1000,
|
||||
tags: [' test tag ']
|
||||
|
@ -213,6 +215,7 @@ describe('Events', () => {
|
|||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
|
||||
expect(response.body.title).toBe('test title 4')
|
||||
expect(response.body.tags[0]).toBe('test tag')
|
||||
})
|
||||
})
|
||||
|
@ -282,10 +285,10 @@ describe('Place', () => {
|
|||
})
|
||||
|
||||
test('admin should get all places', async () => {
|
||||
await request(app).get('/api/place/all')
|
||||
await request(app).get('/api/places')
|
||||
.expect(403)
|
||||
|
||||
const response = await request(app).get('/api/place/all')
|
||||
const response = await request(app).get('/api/places')
|
||||
.auth(token.access_token, { type: 'bearer' })
|
||||
.expect(200)
|
||||
|
||||
|
@ -300,6 +303,17 @@ describe('Place', () => {
|
|||
expect(response.body.length).toBe(2)
|
||||
})
|
||||
|
||||
test('should trim place\'s name and address', async () => {
|
||||
const ret = await request(app).post('/api/event')
|
||||
.send({ title: 'test trimming', place_name: ' test place with white Space ',
|
||||
place_address: ' address with Space ', start_datetime: dayjs().unix() + 1000 })
|
||||
.auth(token.access_token, { type: 'bearer' })
|
||||
.expect(200)
|
||||
|
||||
expect(ret.body.place.name).toBe('test place with white Space')
|
||||
expect(ret.body.place.address).toBe('address with Space')
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
let collections = []
|
||||
|
@ -382,20 +396,14 @@ describe('Collection', () => {
|
|||
})
|
||||
|
||||
test('shoud filter for tags', async () => {
|
||||
let response = await request(app)
|
||||
await request(app)
|
||||
.post('/api/filter')
|
||||
.send({ collectionId: collections[0], tags: ['test'] })
|
||||
.auth(token.access_token, { type: 'bearer' })
|
||||
.expect(200)
|
||||
|
||||
|
||||
// response = await request(app)
|
||||
// .get(`/api/filter/${response.body.id}`)
|
||||
// .auth(token.access_token, { type: 'bearer' })
|
||||
// .expect(200)
|
||||
// expect(response.body.length).toBe(1)
|
||||
|
||||
response = await request(app)
|
||||
const response = await request(app)
|
||||
.get(`/api/collections/test collection`)
|
||||
.expect(200)
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
update()
|
||||
})
|
||||
$: update(
|
||||
maxlength && title && places && tags && theme && show_recurrent && sidebar
|
||||
maxlength && title && places && tags && theme && show_recurrent && sidebar && baseurl
|
||||
)
|
||||
</script>
|
||||
|
||||
|
@ -98,7 +98,7 @@
|
|||
<img
|
||||
style="aspect-ratio=1.7778;"
|
||||
alt={event.title}
|
||||
src={baseurl + '/noimg.svg'}
|
||||
src={baseurl + '/fallbackimage.png'}
|
||||
loading="lazy"
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
Plugin Name: WPGancio
|
||||
Plugin URI: https://gancio.org
|
||||
Description: Connects an user of a gancio instance to a Wordpress user so that published events are automatically pushed with Gancio API.
|
||||
Version: 1.0
|
||||
Version: 1.5
|
||||
Author: Gancio
|
||||
License: AGPL 3.0
|
||||
|
||||
|
@ -19,10 +19,13 @@ You should have received a copy of the GNU Affero General Public License
|
|||
along with (WPGancio). If not, see (https://www.gnu.org/licenses/agpl-3.0.html).
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) or die( 'Nope, not accessing this' );
|
||||
require_once('settings.php');
|
||||
require_once('wc.php');
|
||||
require_once('oauth.php');
|
||||
defined('ABSPATH') or die('Nope, not accessing this');
|
||||
define('WPGANCIO_DIR', plugin_dir_path(__FILE__));
|
||||
require_once(WPGANCIO_DIR . 'settings.php');
|
||||
|
||||
require_once(WPGANCIO_DIR . 'wc.php');
|
||||
require_once(WPGANCIO_DIR . 'oauth.php');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
@ -38,4 +41,3 @@ require_once('oauth.php');
|
|||
* - start an OAuth 2.0 authentication flow with the selected instance
|
||||
* - Send each new / updated events to the selected instance via Gancio API
|
||||
*/
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue