Merge branch 'master' into gh
This commit is contained in:
commit
60e9d95ba8
160 changed files with 7131 additions and 5420 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,6 +1,9 @@
|
|||
# Created by .ignore support plugin (hsz.mobi)
|
||||
|
||||
### Gancio dev configuration
|
||||
*.sqlite
|
||||
releases
|
||||
wp-plugin/wpgancio
|
||||
config/development.json
|
||||
gancio_config.json
|
||||
config.json
|
||||
|
|
96
CHANGELOG
96
CHANGELOG
|
@ -1,6 +1,100 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
|
||||
### Unreleased
|
||||
### 1.2.2 - 7 dic '21
|
||||
- shiny new gancio-event\[s\] webcomponents => [docs](https://gancio.org/usage/embed)
|
||||
- new backend plugin system
|
||||
- improve media focal point selection
|
||||
- improve non-js experience (load img, use native lazy loading)
|
||||
- improve user_confirm / recover code flow
|
||||
- fix task manager exception
|
||||
- fix db initialization when a custom setup is used, #131
|
||||
- remove vue-clipboard2 dependency due to [this](https://github.com/euvl/v-clipboard/issues/18) bug and using a [native with fallback mixin instead](./assets/clipboard.js)
|
||||
- fix a regression to support old CPU, #130
|
||||
- makes dialog use fullscreen on mobile
|
||||
- fix Delete AP Actor Action from fediverse when remote Actor is gone
|
||||
- add `max` param to /events API
|
||||
|
||||
### 1.2.1 - 11 nov '21
|
||||
- fix `Note` remove from fediverse
|
||||
- AP Actor is now `Application`, was `Person`
|
||||
- better handling event AP representations
|
||||
|
||||
this release is a step forward to improve AP compatibility with other platforms, thanks @tcit
|
||||
|
||||
### 1.2.0 - 9 nov '21
|
||||
- do not overwrite event slug when title is modified to preserve links
|
||||
- add public cache to events images
|
||||
- fix baseurl in initial setup configuration
|
||||
- fix user removal
|
||||
- load settings during startup and not for each request
|
||||
- refactoring user custom locale
|
||||
- published AP event's type is not `Note` anymore but `Event`
|
||||
|
||||
### 1.1.1 - 29 ott '21
|
||||
- fix issue adding event with dueHour resulting in `bad request`
|
||||
- fix restart during setup
|
||||
- do not use @nuxt/vuetify module, manually preload vuetify via plugin
|
||||
- remove deprecated nuxt-express-module and use serverMiddleware directly
|
||||
|
||||
### 1.1.0 - 26 ott '21
|
||||
|
||||
- a whole new setup via web! fix #126
|
||||
- new SMTP configuration dialog, fix #115
|
||||
- re-order general settings in admin panel
|
||||
- new dark/light theme setting
|
||||
- move quite all configuration into db
|
||||
- fix some email recipients
|
||||
- fix hidden events when not ended
|
||||
- update translations
|
||||
- improve install documentation
|
||||
- add systemd gancio.service
|
||||
- allow italic and span tags inside editor
|
||||
- remove moment-timezone, consola, config, inquirer dependencies
|
||||
- update deps
|
||||
|
||||
### 1.0.6 (alpha)
|
||||
- fix Dockerfile yarn cache issue on update, #123
|
||||
- fix overflow on event title @homepage
|
||||
- better import dialog on mobile
|
||||
- re-add attachment to AP
|
||||
- fix max event export
|
||||
- update deps
|
||||
|
||||
### 1.0.5 (alpha)
|
||||
- fix/improve debian install docs
|
||||
- fix ics export, use new ics package
|
||||
- use slug url everywhere (rss feed, embedded list)
|
||||
- use i18n in event confirmation email
|
||||
- remove lot of deps warning and remove some unused dependencies
|
||||
- fix show_recurrent in embedded list
|
||||
- remove old to-ico dep, use png favicon instead
|
||||
|
||||
### 1.0.4 (alpha)
|
||||
- shows a generic img for events without it
|
||||
|
||||
### 1.0.3 (alpha)
|
||||
- 12 hour clock selection, #119
|
||||
- improve media management
|
||||
- add alt-text to featured image, fix #106
|
||||
- add focalPoint support, fix #116
|
||||
- improve a11y
|
||||
- improve node v16 compatibility
|
||||
- fix #122 ? (downgrade prettier)
|
||||
|
||||
### 1.0.2 (alpha)
|
||||
- improve oauth flow UI
|
||||
- [WordPress plugin](https://wordpress.org/plugins/wpgancio/)
|
||||
- fix h-event import
|
||||
- improve error logging (add stack trace to exception)
|
||||
- choose start date for recurreing events (#120)
|
||||
- fix user delete from admin
|
||||
|
||||
### 1.0.1 (alpha)
|
||||
|
||||
- fix AP resource removal
|
||||
- improve AP resource UI
|
||||
- fix Docker setup
|
||||
- update deps
|
||||
|
||||
### 1.0 (alpha)
|
||||
This release is a complete rewrite of frontend UI and many internals, main changes are:
|
||||
|
|
11
RELEASE.md
Normal file
11
RELEASE.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
- change version in package.json
|
||||
- add changes to CHANGELOG / changelog.md
|
||||
- yarn build
|
||||
- yarn pack
|
||||
- yarn publish
|
||||
- yarn doc
|
||||
- git add .
|
||||
- git ci -m 'v...'
|
||||
- git tag ...
|
||||
- git push --tags
|
||||
-
|
|
@ -1,3 +0,0 @@
|
|||
export default function (to, from, savedPosition) {
|
||||
return { x: 0, y: 0 }
|
||||
}
|
18
assets/clipboard.js
Normal file
18
assets/clipboard.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
export default {
|
||||
methods: {
|
||||
clipboard (str, msg = 'common.copied') {
|
||||
try {
|
||||
navigator.clipboard.writeText(str)
|
||||
} catch (e) {
|
||||
const el = document.createElement('textarea')
|
||||
el.addEventListener('focusin', e => e.stopPropagation())
|
||||
el.value = str
|
||||
document.body.appendChild(el)
|
||||
el.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(el)
|
||||
}
|
||||
this.$root.$message(msg)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -37,6 +37,9 @@ li {
|
|||
.v-dialog {
|
||||
width: 600px;
|
||||
max-width: 800px;
|
||||
&.v-dialog--fullscreen {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.theme--dark.v-list {
|
||||
|
@ -62,14 +65,14 @@ li {
|
|||
overflow: hidden;
|
||||
|
||||
.title {
|
||||
transition: all .5s;
|
||||
display: block;
|
||||
max-height: 3em;
|
||||
color: white;
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
margin: 0.5rem 1rem 0.5rem 1rem;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
font-size: 1.1em !important;
|
||||
line-height: 1em !important;
|
||||
line-height: 1.2em !important;
|
||||
}
|
||||
|
||||
.body {
|
||||
|
@ -80,9 +83,9 @@ li {
|
|||
width: 100%;
|
||||
max-height: 250px;
|
||||
min-height: 160px;
|
||||
background-color: #222;
|
||||
object-fit: cover;
|
||||
object-position: top;
|
||||
aspect-ratio: 1.7778;
|
||||
}
|
||||
|
||||
.place {
|
||||
|
@ -99,10 +102,6 @@ li {
|
|||
}
|
||||
}
|
||||
|
||||
.v-list {
|
||||
background-color: #333 !important;
|
||||
}
|
||||
|
||||
.vc-past {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
@ -116,4 +115,14 @@ li {
|
|||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.cursorPointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: break-spaces;
|
||||
font-size: 13px;
|
||||
}
|
|
@ -8,6 +8,7 @@
|
|||
:locale='$i18n.locale'
|
||||
:attributes='attributes'
|
||||
transition='fade'
|
||||
aria-label='Calendar'
|
||||
is-expanded
|
||||
is-inline
|
||||
@dayclick='click')
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
|
||||
<template lang="pug">
|
||||
v-dialog(v-model='show'
|
||||
:fullscreen='$vuetify.breakpoint.xsOnly'
|
||||
:color='options.color'
|
||||
:title='title'
|
||||
:max-width='options.width'
|
||||
|
@ -11,8 +12,8 @@
|
|||
v-card-text(v-show='!!message') {{ message }}
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(color='error' @click='cancel') {{$t('common.cancel')}}
|
||||
v-btn(color='primary' @click='agree') {{$t('common.ok')}}
|
||||
v-btn(text color='error' @click='cancel') {{$t('common.cancel')}}
|
||||
v-btn(text color='primary' @click='agree') {{$t('common.ok')}}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template lang='pug'>
|
||||
.editor.grey.darken-4(:class='focused')
|
||||
.editor(:class='focused')
|
||||
.label {{label}}
|
||||
editor-menu-bar.menubar.is-hidden(:editor='editor'
|
||||
:keep-in-bounds='true' v-slot='{ commands, isActive, getMarkAttrs, focused }')
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template lang="pug">
|
||||
v-card.h-event.event.d-flex
|
||||
nuxt-link(:to='`/event/${event.slug || event.id}`')
|
||||
v-img.u-featured.img(:src="`/media/thumb/${event.image_path || 'logo.svg' }`")
|
||||
img.img.u-featured(:src='thumbnail' :alt='alt' loading='lazy' :style="{ 'object-position': thumbnailPosition }")
|
||||
v-icon.float-right.mr-1(v-if='event.parentId' color='success') mdi-repeat
|
||||
.title.p-name {{event.title}}
|
||||
|
||||
|
@ -17,17 +17,16 @@
|
|||
|
||||
v-menu(offset-y)
|
||||
template(v-slot:activator="{on}")
|
||||
v-btn.align-self-end(icon v-on='on' color='primary')
|
||||
v-btn.align-self-end(icon v-on='on' color='primary' alt='more')
|
||||
v-icon mdi-dots-vertical
|
||||
v-list(dense)
|
||||
v-list-item-group
|
||||
v-list-item(v-clipboard:success="() => $root.$message('common.copied', { color: 'success' })"
|
||||
v-clipboard:copy='`${settings.baseurl}/event/${event.id}`')
|
||||
v-list-item(@click='clipboard(`${settings.baseurl}/event/${event.slug || event.id}`)')
|
||||
v-list-item-icon
|
||||
v-icon mdi-content-copy
|
||||
v-list-item-content
|
||||
v-list-item-title {{$t('common.copy_link')}}
|
||||
v-list-item(:href='`/api/event/${event.id}.ics`')
|
||||
v-list-item(:href='`/api/event/${event.slug || event.id}.ics`')
|
||||
v-list-item-icon
|
||||
v-icon mdi-calendar-export
|
||||
v-list-item-content
|
||||
|
@ -45,13 +44,34 @@
|
|||
</template>
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import clipboard from '../assets/clipboard'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
event: { type: Object, default: () => ({}) }
|
||||
},
|
||||
mixins: [clipboard],
|
||||
computed: {
|
||||
...mapState(['settings']),
|
||||
thumbnail () {
|
||||
let path
|
||||
if (this.event.media && this.event.media.length) {
|
||||
path = '/media/thumb/' + this.event.media[0].url
|
||||
} else {
|
||||
path = '/noimg.svg'
|
||||
}
|
||||
return path
|
||||
},
|
||||
alt () {
|
||||
return this.event.media && this.event.media.length ? this.event.media[0].name : ''
|
||||
},
|
||||
thumbnailPosition () {
|
||||
if (this.event.media && this.event.media.length && this.event.media[0].focalpoint) {
|
||||
const focalpoint = this.event.media[0].focalpoint
|
||||
return `${(focalpoint[0] + 1) * 50}% ${(focalpoint[1] + 1) * 50}%`
|
||||
}
|
||||
return 'center center'
|
||||
},
|
||||
is_mine () {
|
||||
if (!this.$auth.user) {
|
||||
return false
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
v-card
|
||||
v-card-title(v-text="$t('common.follow_me_title')")
|
||||
v-card-text
|
||||
p(v-html="$t('event.follow_me_description', { title: settings.title, account: `@${settings.instance_name}@${domain}`})")
|
||||
p(v-html="$t('event.follow_me_description', { title: settings.title, account: `@${settings.instance_name}@${settings.hostname}`})")
|
||||
v-text-field(
|
||||
:rules="[$validators.required('common.url')]"
|
||||
:loading='loading'
|
||||
|
@ -39,10 +39,6 @@ export default {
|
|||
|
||||
computed: {
|
||||
...mapState(['settings']),
|
||||
domain () {
|
||||
const URL = new window.URL(this.settings.baseurl)
|
||||
return URL.hostname
|
||||
},
|
||||
couldGo () {
|
||||
// check if is mastodon
|
||||
this.get_instance_info(this.instance_hostname)
|
||||
|
@ -50,7 +46,7 @@ export default {
|
|||
},
|
||||
link () {
|
||||
// check if exists
|
||||
return `https://${this.instance_hostname}/authorize_interaction?uri=${this.settings.instance_name}@${this.domain}`
|
||||
return `https://${this.instance_hostname}/authorize_interaction?uri=${this.settings.instance_name}@${this.settings.hostname}`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -76,7 +72,7 @@ export default {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
<style>
|
||||
.instance_thumb {
|
||||
height: 20px;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template lang="pug">
|
||||
v-footer(color='secondary')
|
||||
v-footer(aria-label='Footer')
|
||||
|
||||
v-dialog(v-model='showFollowMe' destroy-on-close max-width='700px')
|
||||
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') Gancio <small>{{settings.version}}</small>
|
||||
|
@ -20,7 +20,7 @@
|
|||
:href='instance.url'
|
||||
two-line)
|
||||
v-list-item-avatar
|
||||
v-img(:src='`${instance.url}/favicon.ico`')
|
||||
v-img(:src='`${instance.url}/logo.png`')
|
||||
v-list-item-content
|
||||
v-list-item-title {{instance.name}}
|
||||
v-list-item-subtitle {{instance.label}}
|
||||
|
@ -41,6 +41,7 @@ export default {
|
|||
computed: {
|
||||
...mapState(['settings']),
|
||||
footerLinks () {
|
||||
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 }
|
||||
|
|
|
@ -12,7 +12,6 @@ div#list
|
|||
v-list-item-subtitle <v-icon small color='success' v-if='event.parentId'>mdi-repeat</v-icon> {{event|when}}
|
||||
span.primary--text.ml-1 @{{event.place.name}}
|
||||
v-list-item-title(v-text='event.title')
|
||||
//- a.text-body-1(:href='`/event/${event.id}`' target='_blank') {{event.title}}
|
||||
</template>
|
||||
<script>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template lang="pug">
|
||||
v-app-bar(app)
|
||||
v-app-bar(app aria-label='Menu')
|
||||
|
||||
//- logo, title and description
|
||||
v-list-item(:to='$route.name==="index"?"/about":"/"')
|
||||
|
@ -14,23 +14,23 @@
|
|||
|
||||
v-tooltip(bottom) {{$t('common.add_event')}}
|
||||
template(v-slot:activator='{ on }')
|
||||
v-btn(v-if='could_add' icon nuxt to='/add' v-on='on')
|
||||
v-btn(v-if='could_add' icon nuxt to='/add' v-on='on' :aria-label='$t("common.add_event")')
|
||||
v-icon(large color='primary') mdi-plus
|
||||
|
||||
v-tooltip(bottom) {{$t('common.share')}}
|
||||
template(v-slot:activator='{ on }')
|
||||
v-btn(icon nuxt to='/export' v-on='on')
|
||||
v-btn(icon nuxt to='/export' v-on='on' :aria-label='$t("common.share")')
|
||||
v-icon mdi-share-variant
|
||||
|
||||
v-tooltip(v-if='!$auth.loggedIn' bottom) {{$t('common.login')}}
|
||||
template(v-slot:activator='{ on }')
|
||||
v-btn(icon nuxt to='/login' v-on='on')
|
||||
v-btn(icon nuxt to='/login' v-on='on' :aria-label='$t("common.login")')
|
||||
v-icon mdi-login
|
||||
|
||||
v-menu(v-else
|
||||
offset-y bottom open-on-hover transition="slide-y-transition")
|
||||
template(v-slot:activator="{ on, attrs }")
|
||||
v-btn(icon v-bind='attrs' v-on='on')
|
||||
v-btn(icon v-bind='attrs' v-on='on' aria-label='Menu')
|
||||
v-icon mdi-dots-vertical
|
||||
v-list
|
||||
v-list-item(nuxt to='/settings')
|
||||
|
@ -51,15 +51,17 @@
|
|||
v-list-item-content
|
||||
v-list-item-title {{$t('common.logout')}}
|
||||
|
||||
v-btn(icon v-clipboard:copy='feedLink' v-clipboard:success='copyLink')
|
||||
v-btn(icon @click='clipboard(feedLink, "common.feed_url_copied")' aria-label='RSS')
|
||||
v-icon(color='orange') mdi-rss
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import clipboard from '../assets/clipboard'
|
||||
|
||||
export default {
|
||||
name: 'Nav',
|
||||
mixins: [clipboard],
|
||||
computed: {
|
||||
...mapState(['filters', 'settings']),
|
||||
feedLink () {
|
||||
|
@ -83,9 +85,6 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
copyLink () {
|
||||
this.$root.$message('common.feed_url_copied')
|
||||
},
|
||||
logout () {
|
||||
this.$root.$message('common.logout_ok')
|
||||
this.$auth.logout()
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
v-container
|
||||
v-card-title {{$t('common.announcements')}}
|
||||
v-card-subtitle(v-html="$t('admin.announcement_description')")
|
||||
v-dialog(v-model='dialog' width='800px')
|
||||
v-dialog(v-model='dialog' width='800px' :fullscreen='$vuetify.breakpoint.xsOnly')
|
||||
v-card
|
||||
v-card-title {{$t('admin.new_announcement')}}
|
||||
v-card-text.px-0
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
:headers='headers')
|
||||
template(v-slot:item.actions='{ item }')
|
||||
v-btn(text small @click='confirm(item)' color='success') {{$t('common.confirm')}}
|
||||
v-btn(text small :to='`/event/${item.id}`' color='success') {{$t('common.preview')}}
|
||||
v-btn(text small :to='`/event/${item.slug || item.id}`' color='success') {{$t('common.preview')}}
|
||||
v-btn(text small :to='`/add/${item.id}`' color='warning') {{$t('common.edit')}}
|
||||
v-btn(text small @click='remove(item)'
|
||||
color='error') {{$t('common.delete')}}
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
@blur='save("instance_place", instance_place)'
|
||||
)
|
||||
|
||||
v-dialog(v-model='dialogAddInstance' width="500px")
|
||||
v-dialog(v-model='dialogAddInstance' width='500px' :fullscreen='$vuetify.breakpoint.xsOnly')
|
||||
v-card
|
||||
v-card-title {{$t('admin.add_trusted_instance')}}
|
||||
v-card-text
|
||||
|
|
|
@ -13,8 +13,7 @@
|
|||
dense :headers='instancesHeader'
|
||||
@click:row='instanceSelected')
|
||||
template(v-slot:item.blocked="{ item }")
|
||||
v-icon(v-if='item.blocked') mdi-checkbox-intermediate
|
||||
v-icon(v-else) mdi-checkbox-blank-outline
|
||||
v-icon(@click='toggleBlock(item)') {{item.blocked ? 'mdi-checkbox-intermediate' : 'mdi-checkbox-blank-outline'}}
|
||||
|
||||
v-col(:span='11')
|
||||
span {{$t('common.users')}}
|
||||
|
@ -24,49 +23,39 @@
|
|||
:search='usersFilter'
|
||||
:hide-default-footer='users.length<5'
|
||||
dense :headers='usersHeader')
|
||||
//- template(v-slot:item.username="{item}")
|
||||
//- a(:href='item.ap_id') {{item.object.preferredUsername}}
|
||||
//- el-table-column(:label="$t('common.user')" width='150')
|
||||
//- template(slot-scope='data')
|
||||
//- span(slot='reference')
|
||||
//- a(:href='data.row.object.id' target='_blank') {{data.row.object.name}}
|
||||
//- small ({{data.row.object.preferredUsername}})
|
||||
//- el-table-column(:label="$t('common.resources')" width='90')
|
||||
//- template(slot-scope='data')
|
||||
//- span {{data.row.resources.length}}
|
||||
//- el-table-column(:label="$t('common.actions')" width='200')
|
||||
//- template(slot-scope='data')
|
||||
//- el-button-group
|
||||
//- el-button(size='mini'
|
||||
//- :type='data.row.blocked?"danger":"warning"'
|
||||
//- @click='toggleUserBlock(data.row)') {{data.row.blocked?$t('admin.unblock'):$t('admin.block')}}
|
||||
template(v-slot:item.blocked="{ item }")
|
||||
v-icon(@click='toggleUserBlock(item)') {{item.blocked?'mdi-checkbox-intermediate':'mdi-checkbox-blank-outline'}}
|
||||
|
||||
div
|
||||
v-card-title {{$t('common.resources')}}
|
||||
v-data-table(:items='resources'
|
||||
v-data-table(:items='resources' dense
|
||||
:headers='resourcesHeader'
|
||||
:hide-default-footer='resources.length<10'
|
||||
)
|
||||
//- el-table-column(:label="$t('common.event')")
|
||||
//- template(slot-scope='data')
|
||||
//- span {{data.row.event}}
|
||||
//- el-table-column(:label="$t('common.resources')")
|
||||
//- template(slot-scope='data')
|
||||
//- span(:class='{disabled: data.row.hidden}' v-html='data.row.data.content')
|
||||
//- el-table-column(:label="$t('common.user')" width='200')
|
||||
//- template(slot-scope='data')
|
||||
//- span(:class='{disabled: data.row.hidden}' v-html='data.row.data.actor')
|
||||
//- el-table-column(:label="$t('common.actions')" width="150")
|
||||
//- template(slot-scope='data')
|
||||
//- el-dropdown
|
||||
//- el-button(type="primary" icon="el-icon-arrow-down" size='mini') {{$t('common.moderation')}}
|
||||
//- el-dropdown-menu(slot='dropdown')
|
||||
//- el-dropdown-item(v-if='!data.row.hidden' icon='el-icon-remove' @click.native='hideResource(data.row, true)') {{$t('admin.hide_resource')}}
|
||||
//- el-dropdown-item(v-else icon='el-icon-success' @click.native='hideResource(data.row, false)') {{$t('admin.show_resource')}}
|
||||
//- el-dropdown-item(icon='el-icon-delete' @click.native='deleteResource(data.row)') {{$t('admin.delete_resource')}}
|
||||
//- el-dropdown-item(icon='el-icon-lock' @click.native='toggleUserBlock(data.row.ap_user)') {{$t('admin.block_user')}}
|
||||
:items-per-page='10')
|
||||
template(v-slot:item.content='{ item }')
|
||||
span(v-html='item.data.content')
|
||||
template(v-slot:item.user='{ item }')
|
||||
span {{item.ap_user.preferredUsername}}
|
||||
template(v-slot:item.event='{ item }')
|
||||
span {{item.event.title}}
|
||||
template(v-slot:item.actions='{ item }')
|
||||
v-menu(offset-y)
|
||||
template(v-slot:activator="{ on }")
|
||||
v-btn.mr-2(v-on='on' color='primary' small icon)
|
||||
v-icon mdi-dots-vertical
|
||||
v-list
|
||||
v-list-item(v-if='!item.hidden' @click='hideResource(item, true)')
|
||||
v-list-item-title <v-icon left>mdi-eye-off</v-icon> {{$t('admin.hide_resource')}}
|
||||
v-list-item(v-else @click='hideResource(item, false)')
|
||||
v-list-item-title <v-icon left>mdi-eye</v-icon> {{$t('admin.show_resource')}}
|
||||
v-list-item(@click='deleteResource(item)')
|
||||
v-list-item-title <v-icon left>mdi-delete</v-icon> {{$t('admin.delete_resource')}}
|
||||
//- v-list-item(@click='toggleUserBlock(item.ap_user)')
|
||||
//- v-list-item-title <v-icon left>mdi-lock</v-icon> {{$t('admin.block_user')}}
|
||||
</template>
|
||||
<script>
|
||||
import { mapState, mapActions } from 'vuex'
|
||||
import get from 'lodash/get'
|
||||
|
||||
export default {
|
||||
name: 'Moderation',
|
||||
|
@ -76,7 +65,8 @@ export default {
|
|||
resources: [],
|
||||
users: [],
|
||||
usersHeader: [
|
||||
{ value: 'object.preferredUsername', text: 'Name' }
|
||||
{ value: 'object.preferredUsername', text: 'Name' },
|
||||
{ value: 'blocked', text: 'Blocked' }
|
||||
],
|
||||
instancesHeader: [
|
||||
{ value: 'domain', text: 'Domain' },
|
||||
|
@ -85,44 +75,24 @@ export default {
|
|||
{ value: 'users', text: 'known users' }
|
||||
],
|
||||
resourcesHeader: [
|
||||
{ value: '', text: '' }
|
||||
{ value: 'created', text: 'Created' },
|
||||
{ value: 'event', text: 'Event' },
|
||||
{ value: 'user', text: 'user' },
|
||||
{ value: 'content', text: 'Content' },
|
||||
{ value: 'actions', text: 'Actions' }
|
||||
],
|
||||
usersFilter: '',
|
||||
instancesFilter: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['settings'])
|
||||
// paginatedResources () {
|
||||
// return this.resources.slice((this.resourcePage - 1) * this.perPage,
|
||||
// this.resourcePage * this.perPage)
|
||||
// },
|
||||
// paginatedInstances () {
|
||||
// return this.filteredInstances.slice((this.instancePage - 1) * this.perPage,
|
||||
// this.instancePage * this.perPage)
|
||||
// },
|
||||
// filteredUsers () {
|
||||
// if (!this.usersFilter) { return this.users }
|
||||
// const usersFilter = this.usersFilter.toLowerCase()
|
||||
// return this.users.filter(user => user.name.includes(usersFilter) || user.preferredName.includes(usersFilter))
|
||||
// },
|
||||
// filteredInstances () {
|
||||
// if (!this.instancesFilter) { return this.instances }
|
||||
// const instancesFilter = this.instancesFilter.toLowerCase()
|
||||
// return this.instances.filter(instance =>
|
||||
// (instance.name && instance.name.includes(instancesFilter)) ||
|
||||
// (instance.domain && instance.domain.includes(instancesFilter))
|
||||
// )
|
||||
// },
|
||||
// paginatedSelectedUsers () {
|
||||
// return this.filteredUsers.slice((this.userPage - 1) * this.perPage,
|
||||
// this.userPage * this.perPage)
|
||||
// }
|
||||
},
|
||||
computed: mapState(['settings']),
|
||||
async mounted () {
|
||||
this.instances = await this.$axios.$get('/instances')
|
||||
if (!this.instances.length) {
|
||||
return
|
||||
}
|
||||
this.users = await this.$axios.$get(`/instances/${this.instances[0].domain}`)
|
||||
this.resources = await this.$axios.$get('/resources')
|
||||
// this.users = await this.$axios.$get('/users')
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setSetting']),
|
||||
|
@ -133,6 +103,7 @@ export default {
|
|||
},
|
||||
async instanceSelected (instance) {
|
||||
this.users = await this.$axios.$get(`/instances/${instance.domain}`)
|
||||
this.resources = await this.$axios.$get('/resources', { filters: { instance: instance.domain } })
|
||||
},
|
||||
async hideResource (resource, hidden) {
|
||||
await this.$axios.$put(`/resources/${resource.id}`, { hidden })
|
||||
|
@ -140,7 +111,7 @@ export default {
|
|||
},
|
||||
async toggleUserBlock (ap_user) {
|
||||
if (!ap_user.blocked) {
|
||||
const ret = await this.$root.$confirm('admin.user_block_confirm')
|
||||
const ret = await this.$root.$confirm('admin.user_block_confirm', { user: get(ap_user, 'object.preferredUsername', ap_user.preferredUsername) })
|
||||
if (!ret) { return }
|
||||
}
|
||||
await this.$axios.post('/instances/toggle_user_block', { ap_id: ap_user.ap_id })
|
||||
|
@ -153,6 +124,10 @@ export default {
|
|||
this.resources = this.resources.filter(r => r.id !== resource.id)
|
||||
},
|
||||
async toggleBlock (instance) {
|
||||
if (!instance.blocked) {
|
||||
const ret = await this.$root.$confirm('admin.instance_block_confirm', { instance: instance.domain })
|
||||
if (!ret) { return }
|
||||
}
|
||||
await this.$axios.post('/instances/toggle_block', { instance: instance.domain, blocked: !instance.blocked })
|
||||
instance.blocked = !instance.blocked
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
v-card-title {{$t('common.places')}}
|
||||
v-card-subtitle(v-html="$t('admin.place_description')")
|
||||
|
||||
v-dialog(v-model='dialog' width='600')
|
||||
v-dialog(v-model='dialog' width='600' :fullscreen='$vuetify.breakpoint.xsOnly')
|
||||
v-card(color='secondary')
|
||||
v-card-title {{$t('admin.edit_place')}}
|
||||
v-card-text
|
||||
|
|
77
components/admin/SMTP.vue
Normal file
77
components/admin/SMTP.vue
Normal file
|
@ -0,0 +1,77 @@
|
|||
<template lang="pug">
|
||||
v-card
|
||||
v-card-title SMTP Email configuration
|
||||
v-card-text
|
||||
p(v-html="$t('admin.smtp_description')")
|
||||
|
||||
v-form(v-model='isValid')
|
||||
v-text-field(v-model='admin_email'
|
||||
@blur="save('admin_email', admin_email )"
|
||||
:label="$t('admin.admin_email')"
|
||||
:rules="$validators.email")
|
||||
v-text-field(v-model='smtp.host'
|
||||
:label="$t('admin.smtp_hostname')"
|
||||
:rules="[$validators.required('admin.smtp_hostname')]")
|
||||
|
||||
v-text-field(v-model='smtp.auth.user'
|
||||
:label="$t('common.user')"
|
||||
:rules="[$validators.required('common.user')]")
|
||||
|
||||
v-text-field(v-model='smtp.auth.pass'
|
||||
:label="$t('common.password')"
|
||||
:rules="[$validators.required('common.password')]"
|
||||
type='password')
|
||||
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(color='primary' @click='testSMTP' :loading='loading' :disabled='loading || !isValid') {{$t('admin.smtp_test_button')}}
|
||||
v-btn(color='warning' @click="done") {{$t("common.ok")}}
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import { mapActions, mapState } from 'vuex'
|
||||
export default {
|
||||
data ({ $store }) {
|
||||
const smtp = { host: '', auth: { user: '', pass: '' } }
|
||||
if ($store.state.settings.smtp) {
|
||||
smtp.host = $store.state.settings.smtp.host
|
||||
if ($store.state.settings.smtp.auth) {
|
||||
smtp.auth.user = $store.state.settings.smtp.auth.user
|
||||
smtp.auth.pass = $store.state.settings.smtp.auth.pass
|
||||
}
|
||||
}
|
||||
return {
|
||||
isValid: false,
|
||||
loading: false,
|
||||
smtp,
|
||||
admin_email: $store.state.settings.admin_email || ''
|
||||
}
|
||||
},
|
||||
computed: mapState(['settings']),
|
||||
methods: {
|
||||
...mapActions(['setSetting']),
|
||||
async testSMTP () {
|
||||
this.loading = true
|
||||
try {
|
||||
this.setSetting({ key: 'smtp', value: this.smtp })
|
||||
await this.$axios.$post('/settings/smtp', { smtp: this.smtp })
|
||||
this.$root.$message(this.$t('admin.smtp_test_success', { admin_email: this.admin_email }), { color: 'success' })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.$root.$message(e.response && e.response.data, { color: 'error' })
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
save (key, value) {
|
||||
if (this.settings[key] !== value) {
|
||||
this.setSetting({ key, value })
|
||||
}
|
||||
},
|
||||
done () {
|
||||
this.setSetting({ key: 'smtp', value: JSON.parse(JSON.stringify(this.smtp)) })
|
||||
this.$emit('close')
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -3,19 +3,25 @@
|
|||
v-card-title {{$t('common.settings')}}
|
||||
v-card-text
|
||||
|
||||
v-text-field(v-model='title'
|
||||
:label="$t('common.title')"
|
||||
:hint="$t('admin.title_description')"
|
||||
@blur='save("title", title)'
|
||||
persistent-hint)
|
||||
|
||||
v-text-field.mt-5(v-model='description'
|
||||
:label="$t('common.description')"
|
||||
:hint="$t('admin.description_description')"
|
||||
persistent-hint
|
||||
@blur='save("description", description)')
|
||||
|
||||
//- select timezone
|
||||
v-autocomplete(v-model='instance_timezone'
|
||||
v-autocomplete.mt-5(v-model='instance_timezone'
|
||||
:label="$t('admin.select_instance_timezone')"
|
||||
:hint="$t('admin.instance_timezone_description')"
|
||||
:items="filteredTimezones"
|
||||
persistent-hint
|
||||
item-text='value'
|
||||
item-value='value'
|
||||
placeholder='Timezone, type to search')
|
||||
template(v-slot:item='{ item }')
|
||||
v-list-item-content
|
||||
v-list-item-title {{item.value}}
|
||||
v-list-item-subtitle {{item.offset}}
|
||||
|
||||
v-select.mt-5(
|
||||
v-model='instance_locale'
|
||||
|
@ -25,19 +31,6 @@
|
|||
:items='locales'
|
||||
)
|
||||
|
||||
v-text-field.mt-5(v-model='title'
|
||||
:label="$t('common.title')"
|
||||
:hint="$t('admin.title_description')"
|
||||
@blur='save("title", title)'
|
||||
persistent-hint
|
||||
)
|
||||
|
||||
v-text-field.mt-5(v-model='description'
|
||||
:label="$t('common.description')"
|
||||
:hint="$t('admin.description_description')"
|
||||
persistent-hint
|
||||
@blur='save("description", description)')
|
||||
|
||||
v-switch.mt-4(v-model='allow_registration'
|
||||
inset
|
||||
:label="$t('admin.allow_registration_description')")
|
||||
|
@ -55,24 +48,43 @@
|
|||
inset
|
||||
:label="$t('admin.recurrent_event_visible')")
|
||||
|
||||
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='showSMTPAlert' color='error'>mdi-alert</v-icon> {{$t('admin.show_smtp_setup')}}
|
||||
v-btn(text @click='$emit("complete")' color='primary' v-if='setup') {{$t('common.next')}}
|
||||
v-icon mdi-arrow-right
|
||||
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import SMTP from './SMTP.vue'
|
||||
import { mapActions, mapState } from 'vuex'
|
||||
import moment from 'moment-timezone'
|
||||
import _ from 'lodash'
|
||||
import moment from 'dayjs'
|
||||
import tzNames from './tz.json'
|
||||
import locales from '../../locales/esm'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
setup: { type: Boolean, default: false }
|
||||
},
|
||||
components: { SMTP },
|
||||
name: 'Settings',
|
||||
data ({ $store }) {
|
||||
return {
|
||||
title: $store.state.settings.title,
|
||||
description: $store.state.settings.description,
|
||||
locales: Object.keys(locales).map(locale => ({ value: locale, text: locales[locale] }))
|
||||
locales: Object.keys(locales).map(locale => ({ value: locale, text: locales[locale] })),
|
||||
showSMTP: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['settings']),
|
||||
showSMTPAlert () {
|
||||
return !this.setup && (!this.settings.admin_email || !this.settings.smtp || !this.settings.smtp.host || !this.settings.smtp.user)
|
||||
},
|
||||
instance_locale: {
|
||||
get () { return this.settings.instance_locale },
|
||||
set (value) { this.setSetting({ key: 'instance_locale', value }) }
|
||||
|
@ -99,11 +111,8 @@ export default {
|
|||
},
|
||||
filteredTimezones () {
|
||||
const current_timezone = moment.tz.guess()
|
||||
const ret = _(moment.tz.names())
|
||||
.unshift(current_timezone)
|
||||
.map(tz => ({ value: tz, offset: moment().tz(tz).format('z Z') }))
|
||||
.value()
|
||||
return ret
|
||||
tzNames.unshift(current_timezone)
|
||||
return tzNames
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -9,12 +9,12 @@
|
|||
accept='image/*')
|
||||
template(slot='append-outer')
|
||||
v-btn(color='warning' text @click='resetLogo') <v-icon>mdi-restore</v-icon> {{$t('common.reset')}}
|
||||
v-img(:src='`${settings.baseurl}/favicon.ico?${logoKey}`'
|
||||
max-width="100px" max-height="80px" contain)
|
||||
v-img(:src='`${settings.baseurl}/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='is_dark'
|
||||
inset
|
||||
:label="$t('admin.is_dark')")
|
||||
|
||||
//- TODO choose theme colors
|
||||
//- v-row
|
||||
|
@ -32,7 +32,7 @@
|
|||
//- v-on='on') {{i}}
|
||||
//- v-color-picker(light @update:color='c => updateColor(i, c)')
|
||||
|
||||
v-dialog(v-model='linkModal' width='500')
|
||||
v-dialog(v-model='linkModal' width='500' :fullscreen='$vuetify.breakpoint.xsOnly')
|
||||
v-card
|
||||
v-card-title {{$t('admin.footer_links')}}
|
||||
v-card-text
|
||||
|
@ -52,17 +52,16 @@
|
|||
v-card-text
|
||||
v-btn(color='primary' text @click='openLinkModal') <v-icon>mdi-plus</v-icon> {{$t('admin.add_link')}}
|
||||
v-btn(color='warning' text @click='reset') <v-icon>mdi-restore</v-icon> {{$t('common.reset')}}
|
||||
v-list.mt-1(two-line subheader)
|
||||
v-list-item(v-for='link in settings.footerLinks'
|
||||
:key='`${link.label}`' @click='editFooterLink(link)')
|
||||
v-list-item-content
|
||||
v-list-item-title {{link.label}}
|
||||
v-list-item-subtitle {{link.href}}
|
||||
v-list-item-action
|
||||
//- v-btn.float-right(icon color='accent' @click='editFooterLink(link)')
|
||||
//- v-icon mdi-pencil
|
||||
v-btn(icon color='error' @click.stop='removeFooterLink(link)')
|
||||
v-icon mdi-delete-forever
|
||||
v-card
|
||||
v-list.mt-1(two-line subheader)
|
||||
v-list-item(v-for='link in settings.footerLinks'
|
||||
:key='`${link.label}`' @click='editFooterLink(link)')
|
||||
v-list-item-content
|
||||
v-list-item-title {{link.label}}
|
||||
v-list-item-subtitle {{link.href}}
|
||||
v-list-item-action
|
||||
v-btn(icon color='error' @click.stop='removeFooterLink(link)')
|
||||
v-icon mdi-delete-forever
|
||||
|
||||
</template>
|
||||
<script>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
v-btn(color='primary' text @click='newUserDialog = true') <v-icon>mdi-plus</v-icon> {{$t('common.new_user')}}
|
||||
|
||||
//- ADD NEW USER
|
||||
v-dialog(v-model='newUserDialog' :fullscreen="$vuetify.breakpoint.xsOnly")
|
||||
v-dialog(v-model='newUserDialog' :fullscreen='$vuetify.breakpoint.xsOnly')
|
||||
|
||||
v-card(color='secondary')
|
||||
v-card-title {{$t('common.new_user')}}
|
||||
|
@ -37,6 +37,7 @@
|
|||
v-icon(v-if='item.is_active' color='success') mdi-check
|
||||
v-icon(v-else color='warning') mdi-close
|
||||
template(v-slot:item.actions='{item}')
|
||||
v-btn(v-if='item.recover_code' text small :to='`/user_confirm/${item.recover_code}`') {{$t('common.confirm')}}
|
||||
v-btn(text small @click='toggle(item)'
|
||||
:color='item.is_active?"warning":"success"') {{item.is_active?$t('common.disable'):$t('common.enable')}}
|
||||
v-btn(text small @click='toggleAdmin(item)'
|
||||
|
@ -76,9 +77,16 @@ export default {
|
|||
async deleteUser (user) {
|
||||
const ret = await this.$root.$confirm('admin.delete_user_confirm', { user: user.email })
|
||||
if (!ret) { return }
|
||||
await this.$axios.delete(`/user/${user.id}`)
|
||||
this.$root.$message('admin.user_remove_ok')
|
||||
this.users_ = this.users_.filter(u => u.id !== user.id)
|
||||
try {
|
||||
this.loading = true
|
||||
await this.$axios.$delete(`/user/${user.id}`)
|
||||
this.$root.$message('admin.user_remove_ok')
|
||||
this.$emit('update')
|
||||
} catch (e) {
|
||||
const err = get(e, 'response.data.errors[0].message', e)
|
||||
this.$root.$message(this.$t(err), { color: 'error' })
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async toggle (user) {
|
||||
if (user.is_active) {
|
||||
|
|
595
components/admin/tz.json
Normal file
595
components/admin/tz.json
Normal file
|
@ -0,0 +1,595 @@
|
|||
[
|
||||
"Africa/Abidjan",
|
||||
"Africa/Accra",
|
||||
"Africa/Addis_Ababa",
|
||||
"Africa/Algiers",
|
||||
"Africa/Asmara",
|
||||
"Africa/Asmera",
|
||||
"Africa/Bamako",
|
||||
"Africa/Bangui",
|
||||
"Africa/Banjul",
|
||||
"Africa/Bissau",
|
||||
"Africa/Blantyre",
|
||||
"Africa/Brazzaville",
|
||||
"Africa/Bujumbura",
|
||||
"Africa/Cairo",
|
||||
"Africa/Casablanca",
|
||||
"Africa/Ceuta",
|
||||
"Africa/Conakry",
|
||||
"Africa/Dakar",
|
||||
"Africa/Dar_es_Salaam",
|
||||
"Africa/Djibouti",
|
||||
"Africa/Douala",
|
||||
"Africa/El_Aaiun",
|
||||
"Africa/Freetown",
|
||||
"Africa/Gaborone",
|
||||
"Africa/Harare",
|
||||
"Africa/Johannesburg",
|
||||
"Africa/Juba",
|
||||
"Africa/Kampala",
|
||||
"Africa/Khartoum",
|
||||
"Africa/Kigali",
|
||||
"Africa/Kinshasa",
|
||||
"Africa/Lagos",
|
||||
"Africa/Libreville",
|
||||
"Africa/Lome",
|
||||
"Africa/Luanda",
|
||||
"Africa/Lubumbashi",
|
||||
"Africa/Lusaka",
|
||||
"Africa/Malabo",
|
||||
"Africa/Maputo",
|
||||
"Africa/Maseru",
|
||||
"Africa/Mbabane",
|
||||
"Africa/Mogadishu",
|
||||
"Africa/Monrovia",
|
||||
"Africa/Nairobi",
|
||||
"Africa/Ndjamena",
|
||||
"Africa/Niamey",
|
||||
"Africa/Nouakchott",
|
||||
"Africa/Ouagadougou",
|
||||
"Africa/Porto-Novo",
|
||||
"Africa/Sao_Tome",
|
||||
"Africa/Timbuktu",
|
||||
"Africa/Tripoli",
|
||||
"Africa/Tunis",
|
||||
"Africa/Windhoek",
|
||||
"America/Adak",
|
||||
"America/Anchorage",
|
||||
"America/Anguilla",
|
||||
"America/Antigua",
|
||||
"America/Araguaina",
|
||||
"America/Argentina/Buenos_Aires",
|
||||
"America/Argentina/Catamarca",
|
||||
"America/Argentina/ComodRivadavia",
|
||||
"America/Argentina/Cordoba",
|
||||
"America/Argentina/Jujuy",
|
||||
"America/Argentina/La_Rioja",
|
||||
"America/Argentina/Mendoza",
|
||||
"America/Argentina/Rio_Gallegos",
|
||||
"America/Argentina/Salta",
|
||||
"America/Argentina/San_Juan",
|
||||
"America/Argentina/San_Luis",
|
||||
"America/Argentina/Tucuman",
|
||||
"America/Argentina/Ushuaia",
|
||||
"America/Aruba",
|
||||
"America/Asuncion",
|
||||
"America/Atikokan",
|
||||
"America/Atka",
|
||||
"America/Bahia",
|
||||
"America/Bahia_Banderas",
|
||||
"America/Barbados",
|
||||
"America/Belem",
|
||||
"America/Belize",
|
||||
"America/Blanc-Sablon",
|
||||
"America/Boa_Vista",
|
||||
"America/Bogota",
|
||||
"America/Boise",
|
||||
"America/Buenos_Aires",
|
||||
"America/Cambridge_Bay",
|
||||
"America/Campo_Grande",
|
||||
"America/Cancun",
|
||||
"America/Caracas",
|
||||
"America/Catamarca",
|
||||
"America/Cayenne",
|
||||
"America/Cayman",
|
||||
"America/Chicago",
|
||||
"America/Chihuahua",
|
||||
"America/Coral_Harbour",
|
||||
"America/Cordoba",
|
||||
"America/Costa_Rica",
|
||||
"America/Creston",
|
||||
"America/Cuiaba",
|
||||
"America/Curacao",
|
||||
"America/Danmarkshavn",
|
||||
"America/Dawson",
|
||||
"America/Dawson_Creek",
|
||||
"America/Denver",
|
||||
"America/Detroit",
|
||||
"America/Dominica",
|
||||
"America/Edmonton",
|
||||
"America/Eirunepe",
|
||||
"America/El_Salvador",
|
||||
"America/Ensenada",
|
||||
"America/Fort_Nelson",
|
||||
"America/Fort_Wayne",
|
||||
"America/Fortaleza",
|
||||
"America/Glace_Bay",
|
||||
"America/Godthab",
|
||||
"America/Goose_Bay",
|
||||
"America/Grand_Turk",
|
||||
"America/Grenada",
|
||||
"America/Guadeloupe",
|
||||
"America/Guatemala",
|
||||
"America/Guayaquil",
|
||||
"America/Guyana",
|
||||
"America/Halifax",
|
||||
"America/Havana",
|
||||
"America/Hermosillo",
|
||||
"America/Indiana/Indianapolis",
|
||||
"America/Indiana/Knox",
|
||||
"America/Indiana/Marengo",
|
||||
"America/Indiana/Petersburg",
|
||||
"America/Indiana/Tell_City",
|
||||
"America/Indiana/Vevay",
|
||||
"America/Indiana/Vincennes",
|
||||
"America/Indiana/Winamac",
|
||||
"America/Indianapolis",
|
||||
"America/Inuvik",
|
||||
"America/Iqaluit",
|
||||
"America/Jamaica",
|
||||
"America/Jujuy",
|
||||
"America/Juneau",
|
||||
"America/Kentucky/Louisville",
|
||||
"America/Kentucky/Monticello",
|
||||
"America/Knox_IN",
|
||||
"America/Kralendijk",
|
||||
"America/La_Paz",
|
||||
"America/Lima",
|
||||
"America/Los_Angeles",
|
||||
"America/Louisville",
|
||||
"America/Lower_Princes",
|
||||
"America/Maceio",
|
||||
"America/Managua",
|
||||
"America/Manaus",
|
||||
"America/Marigot",
|
||||
"America/Martinique",
|
||||
"America/Matamoros",
|
||||
"America/Mazatlan",
|
||||
"America/Mendoza",
|
||||
"America/Menominee",
|
||||
"America/Merida",
|
||||
"America/Metlakatla",
|
||||
"America/Mexico_City",
|
||||
"America/Miquelon",
|
||||
"America/Moncton",
|
||||
"America/Monterrey",
|
||||
"America/Montevideo",
|
||||
"America/Montreal",
|
||||
"America/Montserrat",
|
||||
"America/Nassau",
|
||||
"America/New_York",
|
||||
"America/Nipigon",
|
||||
"America/Nome",
|
||||
"America/Noronha",
|
||||
"America/North_Dakota/Beulah",
|
||||
"America/North_Dakota/Center",
|
||||
"America/North_Dakota/New_Salem",
|
||||
"America/Nuuk",
|
||||
"America/Ojinaga",
|
||||
"America/Panama",
|
||||
"America/Pangnirtung",
|
||||
"America/Paramaribo",
|
||||
"America/Phoenix",
|
||||
"America/Port-au-Prince",
|
||||
"America/Port_of_Spain",
|
||||
"America/Porto_Acre",
|
||||
"America/Porto_Velho",
|
||||
"America/Puerto_Rico",
|
||||
"America/Punta_Arenas",
|
||||
"America/Rainy_River",
|
||||
"America/Rankin_Inlet",
|
||||
"America/Recife",
|
||||
"America/Regina",
|
||||
"America/Resolute",
|
||||
"America/Rio_Branco",
|
||||
"America/Rosario",
|
||||
"America/Santa_Isabel",
|
||||
"America/Santarem",
|
||||
"America/Santiago",
|
||||
"America/Santo_Domingo",
|
||||
"America/Sao_Paulo",
|
||||
"America/Scoresbysund",
|
||||
"America/Shiprock",
|
||||
"America/Sitka",
|
||||
"America/St_Barthelemy",
|
||||
"America/St_Johns",
|
||||
"America/St_Kitts",
|
||||
"America/St_Lucia",
|
||||
"America/St_Thomas",
|
||||
"America/St_Vincent",
|
||||
"America/Swift_Current",
|
||||
"America/Tegucigalpa",
|
||||
"America/Thule",
|
||||
"America/Thunder_Bay",
|
||||
"America/Tijuana",
|
||||
"America/Toronto",
|
||||
"America/Tortola",
|
||||
"America/Vancouver",
|
||||
"America/Virgin",
|
||||
"America/Whitehorse",
|
||||
"America/Winnipeg",
|
||||
"America/Yakutat",
|
||||
"America/Yellowknife",
|
||||
"Antarctica/Casey",
|
||||
"Antarctica/Davis",
|
||||
"Antarctica/DumontDUrville",
|
||||
"Antarctica/Macquarie",
|
||||
"Antarctica/Mawson",
|
||||
"Antarctica/McMurdo",
|
||||
"Antarctica/Palmer",
|
||||
"Antarctica/Rothera",
|
||||
"Antarctica/South_Pole",
|
||||
"Antarctica/Syowa",
|
||||
"Antarctica/Troll",
|
||||
"Antarctica/Vostok",
|
||||
"Arctic/Longyearbyen",
|
||||
"Asia/Aden",
|
||||
"Asia/Almaty",
|
||||
"Asia/Amman",
|
||||
"Asia/Anadyr",
|
||||
"Asia/Aqtau",
|
||||
"Asia/Aqtobe",
|
||||
"Asia/Ashgabat",
|
||||
"Asia/Ashkhabad",
|
||||
"Asia/Atyrau",
|
||||
"Asia/Baghdad",
|
||||
"Asia/Bahrain",
|
||||
"Asia/Baku",
|
||||
"Asia/Bangkok",
|
||||
"Asia/Barnaul",
|
||||
"Asia/Beirut",
|
||||
"Asia/Bishkek",
|
||||
"Asia/Brunei",
|
||||
"Asia/Calcutta",
|
||||
"Asia/Chita",
|
||||
"Asia/Choibalsan",
|
||||
"Asia/Chongqing",
|
||||
"Asia/Chungking",
|
||||
"Asia/Colombo",
|
||||
"Asia/Dacca",
|
||||
"Asia/Damascus",
|
||||
"Asia/Dhaka",
|
||||
"Asia/Dili",
|
||||
"Asia/Dubai",
|
||||
"Asia/Dushanbe",
|
||||
"Asia/Famagusta",
|
||||
"Asia/Gaza",
|
||||
"Asia/Harbin",
|
||||
"Asia/Hebron",
|
||||
"Asia/Ho_Chi_Minh",
|
||||
"Asia/Hong_Kong",
|
||||
"Asia/Hovd",
|
||||
"Asia/Irkutsk",
|
||||
"Asia/Istanbul",
|
||||
"Asia/Jakarta",
|
||||
"Asia/Jayapura",
|
||||
"Asia/Jerusalem",
|
||||
"Asia/Kabul",
|
||||
"Asia/Kamchatka",
|
||||
"Asia/Karachi",
|
||||
"Asia/Kashgar",
|
||||
"Asia/Kathmandu",
|
||||
"Asia/Katmandu",
|
||||
"Asia/Khandyga",
|
||||
"Asia/Kolkata",
|
||||
"Asia/Krasnoyarsk",
|
||||
"Asia/Kuala_Lumpur",
|
||||
"Asia/Kuching",
|
||||
"Asia/Kuwait",
|
||||
"Asia/Macao",
|
||||
"Asia/Macau",
|
||||
"Asia/Magadan",
|
||||
"Asia/Makassar",
|
||||
"Asia/Manila",
|
||||
"Asia/Muscat",
|
||||
"Asia/Nicosia",
|
||||
"Asia/Novokuznetsk",
|
||||
"Asia/Novosibirsk",
|
||||
"Asia/Omsk",
|
||||
"Asia/Oral",
|
||||
"Asia/Phnom_Penh",
|
||||
"Asia/Pontianak",
|
||||
"Asia/Pyongyang",
|
||||
"Asia/Qatar",
|
||||
"Asia/Qostanay",
|
||||
"Asia/Qyzylorda",
|
||||
"Asia/Rangoon",
|
||||
"Asia/Riyadh",
|
||||
"Asia/Saigon",
|
||||
"Asia/Sakhalin",
|
||||
"Asia/Samarkand",
|
||||
"Asia/Seoul",
|
||||
"Asia/Shanghai",
|
||||
"Asia/Singapore",
|
||||
"Asia/Srednekolymsk",
|
||||
"Asia/Taipei",
|
||||
"Asia/Tashkent",
|
||||
"Asia/Tbilisi",
|
||||
"Asia/Tehran",
|
||||
"Asia/Tel_Aviv",
|
||||
"Asia/Thimbu",
|
||||
"Asia/Thimphu",
|
||||
"Asia/Tokyo",
|
||||
"Asia/Tomsk",
|
||||
"Asia/Ujung_Pandang",
|
||||
"Asia/Ulaanbaatar",
|
||||
"Asia/Ulan_Bator",
|
||||
"Asia/Urumqi",
|
||||
"Asia/Ust-Nera",
|
||||
"Asia/Vientiane",
|
||||
"Asia/Vladivostok",
|
||||
"Asia/Yakutsk",
|
||||
"Asia/Yangon",
|
||||
"Asia/Yekaterinburg",
|
||||
"Asia/Yerevan",
|
||||
"Atlantic/Azores",
|
||||
"Atlantic/Bermuda",
|
||||
"Atlantic/Canary",
|
||||
"Atlantic/Cape_Verde",
|
||||
"Atlantic/Faeroe",
|
||||
"Atlantic/Faroe",
|
||||
"Atlantic/Jan_Mayen",
|
||||
"Atlantic/Madeira",
|
||||
"Atlantic/Reykjavik",
|
||||
"Atlantic/South_Georgia",
|
||||
"Atlantic/St_Helena",
|
||||
"Atlantic/Stanley",
|
||||
"Australia/ACT",
|
||||
"Australia/Adelaide",
|
||||
"Australia/Brisbane",
|
||||
"Australia/Broken_Hill",
|
||||
"Australia/Canberra",
|
||||
"Australia/Currie",
|
||||
"Australia/Darwin",
|
||||
"Australia/Eucla",
|
||||
"Australia/Hobart",
|
||||
"Australia/LHI",
|
||||
"Australia/Lindeman",
|
||||
"Australia/Lord_Howe",
|
||||
"Australia/Melbourne",
|
||||
"Australia/NSW",
|
||||
"Australia/North",
|
||||
"Australia/Perth",
|
||||
"Australia/Queensland",
|
||||
"Australia/South",
|
||||
"Australia/Sydney",
|
||||
"Australia/Tasmania",
|
||||
"Australia/Victoria",
|
||||
"Australia/West",
|
||||
"Australia/Yancowinna",
|
||||
"Brazil/Acre",
|
||||
"Brazil/DeNoronha",
|
||||
"Brazil/East",
|
||||
"Brazil/West",
|
||||
"CET",
|
||||
"CST6CDT",
|
||||
"Canada/Atlantic",
|
||||
"Canada/Central",
|
||||
"Canada/Eastern",
|
||||
"Canada/Mountain",
|
||||
"Canada/Newfoundland",
|
||||
"Canada/Pacific",
|
||||
"Canada/Saskatchewan",
|
||||
"Canada/Yukon",
|
||||
"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",
|
||||
"Europe/Athens",
|
||||
"Europe/Belfast",
|
||||
"Europe/Belgrade",
|
||||
"Europe/Berlin",
|
||||
"Europe/Bratislava",
|
||||
"Europe/Brussels",
|
||||
"Europe/Bucharest",
|
||||
"Europe/Budapest",
|
||||
"Europe/Busingen",
|
||||
"Europe/Chisinau",
|
||||
"Europe/Copenhagen",
|
||||
"Europe/Dublin",
|
||||
"Europe/Gibraltar",
|
||||
"Europe/Guernsey",
|
||||
"Europe/Helsinki",
|
||||
"Europe/Isle_of_Man",
|
||||
"Europe/Istanbul",
|
||||
"Europe/Jersey",
|
||||
"Europe/Kaliningrad",
|
||||
"Europe/Kiev",
|
||||
"Europe/Kirov",
|
||||
"Europe/Lisbon",
|
||||
"Europe/Ljubljana",
|
||||
"Europe/London",
|
||||
"Europe/Luxembourg",
|
||||
"Europe/Madrid",
|
||||
"Europe/Malta",
|
||||
"Europe/Mariehamn",
|
||||
"Europe/Minsk",
|
||||
"Europe/Monaco",
|
||||
"Europe/Moscow",
|
||||
"Europe/Nicosia",
|
||||
"Europe/Oslo",
|
||||
"Europe/Paris",
|
||||
"Europe/Podgorica",
|
||||
"Europe/Prague",
|
||||
"Europe/Riga",
|
||||
"Europe/Rome",
|
||||
"Europe/Samara",
|
||||
"Europe/San_Marino",
|
||||
"Europe/Sarajevo",
|
||||
"Europe/Saratov",
|
||||
"Europe/Simferopol",
|
||||
"Europe/Skopje",
|
||||
"Europe/Sofia",
|
||||
"Europe/Stockholm",
|
||||
"Europe/Tallinn",
|
||||
"Europe/Tirane",
|
||||
"Europe/Tiraspol",
|
||||
"Europe/Ulyanovsk",
|
||||
"Europe/Uzhgorod",
|
||||
"Europe/Vaduz",
|
||||
"Europe/Vatican",
|
||||
"Europe/Vienna",
|
||||
"Europe/Vilnius",
|
||||
"Europe/Volgograd",
|
||||
"Europe/Warsaw",
|
||||
"Europe/Zagreb",
|
||||
"Europe/Zaporozhye",
|
||||
"Europe/Zurich",
|
||||
"GB",
|
||||
"GB-Eire",
|
||||
"GMT",
|
||||
"GMT+0",
|
||||
"GMT-0",
|
||||
"GMT0",
|
||||
"Greenwich",
|
||||
"HST",
|
||||
"Hongkong",
|
||||
"Iceland",
|
||||
"Indian/Antananarivo",
|
||||
"Indian/Chagos",
|
||||
"Indian/Christmas",
|
||||
"Indian/Cocos",
|
||||
"Indian/Comoro",
|
||||
"Indian/Kerguelen",
|
||||
"Indian/Mahe",
|
||||
"Indian/Maldives",
|
||||
"Indian/Mauritius",
|
||||
"Indian/Mayotte",
|
||||
"Indian/Reunion",
|
||||
"Iran",
|
||||
"Israel",
|
||||
"Jamaica",
|
||||
"Japan",
|
||||
"Kwajalein",
|
||||
"Libya",
|
||||
"MET",
|
||||
"MST",
|
||||
"MST7MDT",
|
||||
"Mexico/BajaNorte",
|
||||
"Mexico/BajaSur",
|
||||
"Mexico/General",
|
||||
"NZ",
|
||||
"NZ-CHAT",
|
||||
"Navajo",
|
||||
"PRC",
|
||||
"PST8PDT",
|
||||
"Pacific/Apia",
|
||||
"Pacific/Auckland",
|
||||
"Pacific/Bougainville",
|
||||
"Pacific/Chatham",
|
||||
"Pacific/Chuuk",
|
||||
"Pacific/Easter",
|
||||
"Pacific/Efate",
|
||||
"Pacific/Enderbury",
|
||||
"Pacific/Fakaofo",
|
||||
"Pacific/Fiji",
|
||||
"Pacific/Funafuti",
|
||||
"Pacific/Galapagos",
|
||||
"Pacific/Gambier",
|
||||
"Pacific/Guadalcanal",
|
||||
"Pacific/Guam",
|
||||
"Pacific/Honolulu",
|
||||
"Pacific/Johnston",
|
||||
"Pacific/Kiritimati",
|
||||
"Pacific/Kosrae",
|
||||
"Pacific/Kwajalein",
|
||||
"Pacific/Majuro",
|
||||
"Pacific/Marquesas",
|
||||
"Pacific/Midway",
|
||||
"Pacific/Nauru",
|
||||
"Pacific/Niue",
|
||||
"Pacific/Norfolk",
|
||||
"Pacific/Noumea",
|
||||
"Pacific/Pago_Pago",
|
||||
"Pacific/Palau",
|
||||
"Pacific/Pitcairn",
|
||||
"Pacific/Pohnpei",
|
||||
"Pacific/Ponape",
|
||||
"Pacific/Port_Moresby",
|
||||
"Pacific/Rarotonga",
|
||||
"Pacific/Saipan",
|
||||
"Pacific/Samoa",
|
||||
"Pacific/Tahiti",
|
||||
"Pacific/Tarawa",
|
||||
"Pacific/Tongatapu",
|
||||
"Pacific/Truk",
|
||||
"Pacific/Wake",
|
||||
"Pacific/Wallis",
|
||||
"Pacific/Yap",
|
||||
"Poland",
|
||||
"Portugal",
|
||||
"ROC",
|
||||
"ROK",
|
||||
"Singapore",
|
||||
"Turkey",
|
||||
"UCT",
|
||||
"US/Alaska",
|
||||
"US/Aleutian",
|
||||
"US/Arizona",
|
||||
"US/Central",
|
||||
"US/East-Indiana",
|
||||
"US/Eastern",
|
||||
"US/Hawaii",
|
||||
"US/Indiana-Starke",
|
||||
"US/Michigan",
|
||||
"US/Mountain",
|
||||
"US/Pacific",
|
||||
"US/Samoa",
|
||||
"UTC",
|
||||
"Universal",
|
||||
"W-SU",
|
||||
"WET",
|
||||
"Zulu"
|
||||
]
|
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
"title": "Gancio",
|
||||
"description": "A shared agenda for local communities",
|
||||
"baseurl": "http://localhost:13120",
|
||||
"server": {
|
||||
"host": "localhost",
|
||||
"port": 13120
|
||||
},
|
||||
"log_level": "debug",
|
||||
"log_path": "./logs",
|
||||
"db": {
|
||||
"dialect": "sqlite",
|
||||
"storage": "./db.sqlite",
|
||||
"logging": false
|
||||
},
|
||||
"upload_path": "./",
|
||||
"smtp": {
|
||||
"auth": {
|
||||
"user": "",
|
||||
"pass": ""
|
||||
},
|
||||
"secure": true,
|
||||
"host": ""
|
||||
},
|
||||
"admin_email": "admin"
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
// DO NOT TOUCH THIS FILE
|
||||
const fs = require('fs')
|
||||
const config_path = process.env.config_path
|
||||
|
||||
let config = {}
|
||||
if (fs.existsSync(config_path)) {
|
||||
config = require(config_path)
|
||||
}
|
||||
|
||||
module.exports = config
|
|
@ -34,11 +34,9 @@ plugins:
|
|||
search_enabled: true
|
||||
|
||||
aux_links:
|
||||
"Blog":
|
||||
- https://blog.gancio.org
|
||||
"Source":
|
||||
- https://framagit.org/les/gancio
|
||||
"Mastodon":
|
||||
"@gancio@mastodon.cisti.org":
|
||||
- https://mastodon.cisti.org/@gancio
|
||||
|
||||
gh_edit_link: true # show or hide edit this page link
|
||||
|
|
|
@ -8,6 +8,104 @@ nav_order: 10
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
### 1.2.2 - 7 dic '21
|
||||
- shiny new gancio-event\[s\] webcomponents => [docs](https://gancio.org/usage/embed)
|
||||
- new backend plugin system
|
||||
- improve media focal point selection
|
||||
- improve non-js experience (load img, use native lazy loading)
|
||||
- improve user_confirm / recover code flow
|
||||
- fix task manager exception
|
||||
- fix db initialization when a custom setup is used, #131
|
||||
- remove vue-clipboard2 dependency due to [this](https://github.com/euvl/v-clipboard/issues/18) bug and using a [native with fallback mixin instead](./assets/clipboard.js)
|
||||
- fix a regression to support old CPU, #130
|
||||
- makes dialog use fullscreen on mobile
|
||||
- fix Delete AP Actor Action from fediverse when remote Actor is gone
|
||||
- add `max` param to /events API
|
||||
|
||||
|
||||
### 1.2.1 - 11 nov '21
|
||||
- fix `Note` remove from fediverse
|
||||
- AP Actor is now `Application`, was `Person`
|
||||
- better handling event AP representations
|
||||
|
||||
this release is a step forward to improve AP compatibility with other platforms, thanks @tcit
|
||||
|
||||
|
||||
### 1.2.0 - 9 nov '21
|
||||
- do not overwrite event slug when title is modified to preserve links
|
||||
- add public cache to events images
|
||||
- fix baseurl in initial setup configuration
|
||||
- fix user removal
|
||||
- load settings during startup and not for each request
|
||||
- refactoring user custom locale
|
||||
- published AP event's type is not `Note` anymore but `Event`
|
||||
|
||||
### 1.1.1 - 29 ott '21
|
||||
- fix issue adding event with dueHour resulting in `bad request`
|
||||
- fix restart during setup
|
||||
- do not use @nuxt/vuetify module, manually preload vuetify via plugin
|
||||
- remove deprecated nuxt-express-module and use serverMiddleware directly
|
||||
|
||||
### 1.1.0 - 26 ott '21
|
||||
|
||||
- a whole new setup via web! fix #126
|
||||
- new SMTP configuration dialog, fix #115
|
||||
- re-order general settings in admin panel
|
||||
- new dark/light theme setting
|
||||
- move quite all configuration into db
|
||||
- fix some email recipients
|
||||
- fix hidden events when not ended
|
||||
- update translations
|
||||
- improve install documentation
|
||||
- add systemd gancio.service
|
||||
- allow italic and span tags inside editor
|
||||
- remove moment-timezone, consola, config, inquirer dependencies
|
||||
- update deps
|
||||
|
||||
### 1.0.6 (alpha)
|
||||
- fix Dockerfile yarn cache issue on update, #123
|
||||
- fix overflow on event title @homepage
|
||||
- better import dialog on mobile
|
||||
- re-add attachment to AP
|
||||
- fix max event export
|
||||
- update deps
|
||||
|
||||
### 1.0.5 (alpha)
|
||||
- fix/improve debian install docs
|
||||
- fix ics export, use new ics package
|
||||
- use slug url everywhere (rss feed, embedded list)
|
||||
- use i18n in event confirmation email
|
||||
- remove lot of deps warning and remove some unused dependencies
|
||||
- fix show_recurrent in embedded list
|
||||
- remove old to-ico dep, use png favicon instead
|
||||
|
||||
### 1.0.4 (alpha)
|
||||
- shows a generic img for events without it
|
||||
|
||||
### 1.0.3 (alpha)
|
||||
- 12 hour clock selection, #119
|
||||
- improve media management
|
||||
- add alt-text to featured image, fix #106
|
||||
- add focalPoint support, fix #116
|
||||
- improve a11y
|
||||
- improve node v16 compatibility
|
||||
- fix #122 ? (downgrade prettier)
|
||||
|
||||
### 1.0.2 (alpha)
|
||||
- improve oauth flow UI
|
||||
- [WordPress plugin](https://wordpress.org/plugins/wpgancio/)
|
||||
- fix h-event import
|
||||
- improve error logging (add stack trace to exception)
|
||||
- choose start date for recurreing events (#120)
|
||||
- fix user delete from admin
|
||||
|
||||
### 1.0.1 (alpha)
|
||||
|
||||
- fix AP resource removal
|
||||
- improve AP resource UI
|
||||
- fix Docker setup
|
||||
- update deps
|
||||
|
||||
### 1.0 (alpha)
|
||||
This release is a complete rewrite of frontend UI and many internals, main changes are:
|
||||
|
||||
|
|
|
@ -7,5 +7,8 @@ nav_order: 9
|
|||
|
||||
## Contacts
|
||||
|
||||
### :elephant: Mastodon ⇒ [@gancio@mastodon.cisti.org](https://mastodon.cisti.org/@gancio)
|
||||
|
||||
- :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...)
|
||||
|
||||
|
|
|
@ -1,2 +1,7 @@
|
|||
FROM node:buster
|
||||
RUN yarn global add --silent https://gancio.org/latest.tgz 2> /dev/null
|
||||
RUN yarn global remove gancio || true
|
||||
RUN yarn cache clean
|
||||
RUN yarn global add --latest --production --silent https://gancio.org/latest.tgz 2> /dev/null
|
||||
ADD entrypoint.sh /
|
||||
RUN chmod 755 /entrypoint.sh
|
||||
ENTRYPOINT [ "/bin/sh", "/entrypoint.sh" ]
|
||||
|
|
4
docs/docker/entrypoint.sh
Executable file
4
docs/docker/entrypoint.sh
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
chown -R node:node /home/node
|
||||
su node -c "$*"
|
||||
|
|
@ -19,12 +19,13 @@ services:
|
|||
build: .
|
||||
restart: always
|
||||
image: node:buster
|
||||
user: node
|
||||
container_name: gancio
|
||||
environment:
|
||||
- PATH=$PATH:/home/node/.yarn/bin
|
||||
- GANCIO_DATA=/home/node/data
|
||||
- NODE_ENV=production
|
||||
command: gancio start --docker
|
||||
entrypoint: /entrypoint.sh
|
||||
volumes:
|
||||
- ./data:/home/node/data
|
||||
ports:
|
||||
|
|
|
@ -5,11 +5,12 @@ services:
|
|||
build: .
|
||||
restart: always
|
||||
image: node:buster
|
||||
user: node
|
||||
container_name: gancio
|
||||
environment:
|
||||
- PATH=$PATH:/home/node/.yarn/bin
|
||||
- GANCIO_DATA=/home/node/data
|
||||
- NODE_ENV=production
|
||||
entrypoint: /entrypoint.sh
|
||||
command: gancio start --docker
|
||||
volumes:
|
||||
- ./data:/home/node/data
|
||||
|
|
33
docs/embed.md
Normal file
33
docs/embed.md
Normal file
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
layout: default
|
||||
title: Embed events
|
||||
permalink: /usage/embed
|
||||
nav_order: 1
|
||||
parent: Usage
|
||||
---
|
||||
|
||||
## Embed event
|
||||
|
||||
You can embed a list of filtered events or a specific event card in your webpage using a classic old-school `iframe` or a shiny new webcomponent.
|
||||
|
||||
### Webcomponents
|
||||
The webcomponent require a small js to be loaded in your page:
|
||||
```javascript
|
||||
<script src='https://demo.gancio.org/gancio-events.es.js'></script>
|
||||
```
|
||||
|
||||
#### embed a single event
|
||||
> you can copy the code in **event page > Embed > Copy**
|
||||
|
||||
<script src='https://demo.gancio.org/gancio-events.es.js'></script>
|
||||
<gancio-event id=17 baseurl='https://demo.gancio.org'></gancio-event>
|
||||
```javascript
|
||||
<gancio-event id=17 baseurl='https://demo.gancio.org'></gancio-event>
|
||||
```
|
||||
|
||||
#### embed event lists
|
||||
> you can copy the code in **Export > List > Copy**
|
||||
<gancio-events baseurl='https://demo.gancio.org'></gancio-events>
|
||||
```javascript
|
||||
<gancio-event baseurl='https://demo.gancio.org'></gancio-event>
|
||||
```
|
|
@ -5,12 +5,8 @@ permalink: /federation
|
|||
nav_order: 9
|
||||
---
|
||||
|
||||
## Federation
|
||||
## Federation / ActivityPub
|
||||
|
||||
Each instance has only one [AP Actor](https://www.w3.org/TR/activitypub/#actors) that publishes each event.
|
||||
We are considering the introduction of other “Actor” but they will not be linked to users, rather to places or tags/categories.
|
||||
There are no personal homes with a timeline of people I follow, everyone has a sort of local timeline of the instance, it’s an anti filter-bubble feature.
|
||||
|
||||
Events are not published with the type `Event` but with type `Note` because we wanted to add the possibility to interact with events from mastodon instances (boost / bookmark and “comments” that we call resources because we don’t want it to become a place of debate, but more a place where to keep a historical memory of events, e.g. an audio recording of a talk).
|
||||
|
||||
When mastodon will support `Event` object type we will change for sure.
|
||||
Each instance has only one [AP Actor](https://www.w3.org/TR/activitypub/#actors) of type `Application` named `gancio@instance.tld` that publishes each event.
|
||||
We are considering the introduction of other `Actor` but they will not be linked to users, rather to places or tags/categories.
|
||||
There are no personal homes with a timeline of people you follow, everyone has a sort of local timeline of the instance, it’s an anti filter-bubble feature.
|
||||
|
|
13
docs/gancio.service
Normal file
13
docs/gancio.service
Normal file
|
@ -0,0 +1,13 @@
|
|||
[Unit]
|
||||
Description=Gancio
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=gancio
|
||||
WorkingDirectory=/opt/gancio
|
||||
ExecStart=gancio
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
|
@ -8,16 +8,43 @@ parent: Install
|
|||
|
||||
## Backup
|
||||
|
||||
The following commands should be valid for every setup (docker/debian/sqlite/postgres) but check your installation directory first.
|
||||
This includes database, configuration, custom user locales, logs, images and thumbnails.
|
||||
The following commands should be valid for every setup (docker/debian/sqlite/postgres).
|
||||
|
||||
1. Move to gancio path
|
||||
```bash
|
||||
cd /opt/gancio/ # or /home/gancio or where your installation is
|
||||
tar -czf gancio-$(date +%Y-%m-%d-%H%M%S)-backup.tgz \
|
||||
$(ls -d config.json uploads user_locale db.sqlite postgres data logs 2> /dev/null)
|
||||
cd /opt/gancio/ # or where your installation is
|
||||
```
|
||||
|
||||
1. Backup PostgreSQL (only required for non-docker PostgreSQL installation)
|
||||
```bash
|
||||
sudo -u postgres pg_dump -Fc gancio > gancio.dump
|
||||
```
|
||||
|
||||
1. Archive database, configuration, custom user locales, logs, images and thumbnails
|
||||
```bash
|
||||
sudo tar -czf gancio-$(date +%Y-%m-%d-%H%M%S)-backup.tgz \
|
||||
$(ls -d config.json uploads user_locale db.sqlite gancio.dump postgres data logs 2> /dev/null)
|
||||
```
|
||||
> warning "Permission denied"
|
||||
> `postgres` directory could have different permission or owner, in this case you need to be root or use `sudo` instead.
|
||||
|
||||
> info "Automatic backup"
|
||||
> To periodically backup your data you should probably use something like [restic](https://restic.net) or [borg](https://www.borgbackup.org/)
|
||||
> To periodically backup your data you should probably use something like [restic](https://restic.net) or [borg](https://www.borgbackup.org/)
|
||||
|
||||
|
||||
## Restore
|
||||
|
||||
1. Install a clean gancio
|
||||
1. Move to gancio path
|
||||
```bash
|
||||
cd /opt/gancio/ # or where your installation is
|
||||
```
|
||||
|
||||
1. Extract your backup
|
||||
```bash
|
||||
tar xvf gancio-*-backup.tgz
|
||||
```
|
||||
|
||||
1. Restore PostgreSQL database (only required for non-docker PostgreSQL installation)
|
||||
```
|
||||
sudo -u postgres createdb gancio
|
||||
sudo -u postgres pg_restore -d gancio gancio.dump
|
||||
```
|
|
@ -16,22 +16,8 @@ The configuration file shoud be a `.json` or a `.js` file and could be specified
|
|||
1. TOC
|
||||
{:toc}
|
||||
|
||||
- ### Title
|
||||
The title will be in rss feed, in html head and in emails:
|
||||
|
||||
`"title": "Gancio"`
|
||||
|
||||
![title](../assets/title.png)
|
||||
|
||||
- ### Description
|
||||
`"description": "a shared agenda for local communities"`
|
||||
|
||||
- ### BaseURL
|
||||
URL where your site will be accessible (include http or https):
|
||||
`"baseurl": "https://gancio.cisti.org"`
|
||||
|
||||
- ### Server
|
||||
This probably support unix socket too :D
|
||||
This probably support unix socket too
|
||||
|
||||
```json
|
||||
"server": {
|
||||
|
@ -52,78 +38,28 @@ DB configuration, look [here](https://sequelize.org/master/class/lib/sequelize.j
|
|||
Where to save images
|
||||
`"upload_path": "./uploads"`
|
||||
|
||||
- ### SMTP
|
||||
SMTP configuration.
|
||||
Gancio should send emails at following events:
|
||||
- the admin should receive emails of anon event (if enabled) to confirm them.
|
||||
- the admin should receive emails of registration request (if enabled) to confirm them.
|
||||
- an user should receive an email of registration requested.
|
||||
- an user should receive an email of confirmed registration.
|
||||
- an user should receive a confirmation email when subscribed directly by admin.
|
||||
|
||||
```json
|
||||
"smtp": {
|
||||
"auth": {
|
||||
"user": "",
|
||||
"pass": ""
|
||||
},
|
||||
"secure": true,
|
||||
"host": ""
|
||||
}
|
||||
```
|
||||
|
||||
- ### Admin_email
|
||||
Email of administrator. Note that email from gancio comes from this email and that
|
||||
the SMTP configuration above should allow to use this address as from.
|
||||
|
||||
|
||||
- ### 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.
|
||||
For example, let's say you want to modify the text inside the `/about`
|
||||
page:
|
||||
`mkdir /opt/gancio/user_locale`
|
||||
put something like this in `/opt/gancio/user_locale/en.js` to override the about in
|
||||
For example, let's say you want to modify the text shown during registration:
|
||||
`mkdir /opt/gancio/user_locale`
|
||||
|
||||
put something like this in `/opt/gancio/user_locale/en.json` to override the registration description in
|
||||
english:
|
||||
```js
|
||||
export default {
|
||||
about: 'A new about'
|
||||
```json
|
||||
{
|
||||
"registrer": {
|
||||
"description": "My new registration page description"
|
||||
}
|
||||
}
|
||||
```
|
||||
and then point the `user_locale` configuration to that directory:
|
||||
and then point the `user_locale` configuration to that directory (in your `config.json`):
|
||||
```json
|
||||
"user_locale": "/opt/gancio/user_locale"
|
||||
```
|
||||
Watch [here](https://framagit.org/les/gancio/tree/master/locales) for a
|
||||
list of strings you can override.
|
||||
<small>:warning: Note that a restart is needed when you change
|
||||
user_locale's content.</small>
|
||||
list of strings you can override.
|
||||
|
||||
|
||||
## Default settings
|
||||
```json
|
||||
{
|
||||
"title": "Gancio",
|
||||
"description": "A shared agenda for local communities",
|
||||
"baseurl": "http://localhost:13120",
|
||||
"server": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 13120
|
||||
},
|
||||
"db": {
|
||||
"dialect": "sqlite",
|
||||
"storage": "./db.sqlite"
|
||||
},
|
||||
"upload_path": "./",
|
||||
"smtp": {
|
||||
"auth": {
|
||||
"user": "",
|
||||
"pass": ""
|
||||
},
|
||||
"secure": true,
|
||||
"host": ""
|
||||
},
|
||||
"admin_email": "",
|
||||
}
|
||||
```
|
||||
> warning "Restart needed"
|
||||
> Note that a restart is needed when you change user_locale's content.
|
||||
|
|
|
@ -7,24 +7,24 @@ parent: Install
|
|||
|
||||
## Debian installation
|
||||
|
||||
1. Install Node.js & yarn (**from root**)
|
||||
1. Install dependencies
|
||||
```bash
|
||||
curl -sL https://deb.nodesource.com/setup_16.x | bash -
|
||||
apt-get install -y nodejs
|
||||
curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list
|
||||
apt-get update && apt-get install yarn
|
||||
sudo apt install curl gcc g++ make wget libpq-dev
|
||||
```
|
||||
|
||||
|
||||
1. Install Node.js & yarn
|
||||
```bash
|
||||
curl -sL https://deb.nodesource.com/setup_16.x | sudo bash -
|
||||
sudo apt-get install -y nodejs
|
||||
sudo npm install -g yarn
|
||||
```
|
||||
<small>[source](https://github.com/nodesource/distributions/blob/master/README.md)</small>
|
||||
|
||||
1. Install Gancio
|
||||
```bash
|
||||
yarn global add --silent {{site.url}}{% link /latest.tgz %} 2> /dev/null
|
||||
```
|
||||
|
||||
1. Setup with postgreSQL __(optional as you can choose sqlite)__
|
||||
```bash
|
||||
apt-get install postgresql
|
||||
sudo apt-get install postgresql
|
||||
# Create the database
|
||||
su postgres -c psql
|
||||
postgres=# create database gancio;
|
||||
|
@ -34,36 +34,38 @@ postgres=# grant all privileges on database gancio to gancio;
|
|||
|
||||
1. Create a user to run gancio from
|
||||
```bash
|
||||
adduser gancio
|
||||
su gancio
|
||||
sudo adduser --group --system --shell /bin/false --home /opt/gancio gancio
|
||||
```
|
||||
1. Install Gancio
|
||||
```bash
|
||||
sudo yarn global add --silent {{site.url}}/latest.tgz 2> /dev/null
|
||||
```
|
||||
|
||||
1. Launch interactive setup
|
||||
1. Setup systemd service and reload systemd
|
||||
```bash
|
||||
gancio setup --config config.json
|
||||
sudo wget http://gancio.org/gancio.service -O /etc/systemd/system/gancio.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable gancio
|
||||
```
|
||||
|
||||
1. Start
|
||||
1. Start gancio service (this should listen on port 13120)
|
||||
```bash
|
||||
gancio start --config config.json
|
||||
sudo systemctl start gancio
|
||||
```
|
||||
1. Point your web browser to [http://localhost:13120](http://localhost:13120) or where you selected during setup.
|
||||
|
||||
1. [Setup nginx as a proxy]({% link install/nginx.md %})
|
||||
|
||||
1. To deploy gancio in production you should use something like **[pm2](http://pm2.keymetrics.io/)**:
|
||||
|
||||
```bash
|
||||
sudo yarn global add pm2
|
||||
pm2 start gancio -- --config config.json
|
||||
|
||||
# Run this command to run your application as a service and automatically restart after a reboot:
|
||||
pm2 startup # read the output!
|
||||
sudo pm2 startup -u gancio
|
||||
```
|
||||
1. Point your web browser to your domain :tada:
|
||||
|
||||
## Upgrade
|
||||
|
||||
> warning "Backup your data"
|
||||
> Backup your data is generally a good thing to do and this is especially true before upgrading.
|
||||
> Don't be lazy and [backup]({% link install/backup.md %}) your data!
|
||||
|
||||
```bash
|
||||
sudo yarn global add --silent {{site.url}}{% link /latest.tgz %} 2> /dev/null
|
||||
sudo service pm2 restart
|
||||
yarn global remove gancio
|
||||
yarn cache clean
|
||||
yarn global add --silent {{site.url}}/latest.tgz 2> /dev/null
|
||||
sudo service gancio restart
|
||||
```
|
||||
|
|
|
@ -13,28 +13,37 @@ nav_order: 2
|
|||
|
||||
## Initial setup
|
||||
|
||||
> info "Clone not needed"
|
||||
> You do not need to clone the full repo, a `Dockerfile` and a `docker-compose.yml` are enough.
|
||||
|
||||
- __You must have the following dependencies installed: Docker, Docker Compose and Nginx__
|
||||
|
||||
```bash
|
||||
sudo apt install docker docker-compose nginx
|
||||
```
|
||||
or
|
||||
1. [Install docker](https://docs.docker.com/engine/install/)
|
||||
1. [Install docker-compose](https://docs.docker.com/compose/install/)
|
||||
1. [Install nginx](https://nginx.org/en/docs/install.html)
|
||||
|
||||
- __Create a directory where everything related to gancio is stored__
|
||||
```bash
|
||||
mkdir -p /opt/gancio/data
|
||||
mkdir -p /opt/gancio
|
||||
cd /opt/gancio
|
||||
```
|
||||
|
||||
## Use sqlite
|
||||
<div class='code-example bg-grey-lt-100' markdown="1">
|
||||
|
||||
1. **Download docker-compose.yml and Dockerfile**
|
||||
```bash
|
||||
wget {{site.url}}{% link /docker/Dockerfile %}
|
||||
wget {{site.url}}{% link /docker/entrypoint.sh %}
|
||||
wget {{site.url}}{% link /docker/sqlite/docker-compose.yml %}
|
||||
```
|
||||
|
||||
|
||||
1. Build docker image and launch interactive setup
|
||||
1. Build docker image
|
||||
```
|
||||
docker-compose build
|
||||
docker-compose run --rm gancio gancio setup --docker --db=sqlite
|
||||
```
|
||||
</div>
|
||||
|
||||
|
@ -44,13 +53,13 @@ docker-compose run --rm gancio gancio setup --docker --db=sqlite
|
|||
1. **Download docker-compose.yml and Dockerfile**
|
||||
```bash
|
||||
wget {{site.url}}{% link /docker/Dockerfile %}
|
||||
wget {{site.url}}{% link /docker/entrypoint.sh %}
|
||||
wget {{site.url}}{% link /docker/postgres/docker-compose.yml %}
|
||||
```
|
||||
|
||||
1. Build docker image and launch interactive setup
|
||||
1. Build docker image
|
||||
```
|
||||
docker-compose build
|
||||
docker-compose run --rm gancio gancio setup --docker --db=postgres
|
||||
```
|
||||
</div>
|
||||
|
||||
|
@ -67,9 +76,9 @@ docker-compose up -d
|
|||
tail -f data/logs/gancio.log
|
||||
```
|
||||
|
||||
1. [Setup nginx as a proxy]({% link install/nginx.md %}
|
||||
1. [Setup nginx as a proxy]({% link install/nginx.md %})
|
||||
|
||||
1. Point your web browser to [http://localhost:13120](http://localhost:13120) or where you specified during setup and enjoy :tada:
|
||||
1. Point your web browser to your domain :tada:
|
||||
|
||||
1. Edit `data/config.json` and restart the container on your needs, see [Configuration]({% link install/configuration.md %}) for more details.
|
||||
|
||||
|
@ -86,6 +95,7 @@ tail -f data/logs/gancio.log
|
|||
> 1. `cd /opt/gancio`
|
||||
> 1. [Backup your data]({% link install/backup.md %})
|
||||
> 1. Download new `Dockerfile` <br/> `wget {{site.url}}{% link /docker/Dockerfile %}`
|
||||
> 1. Download new `entrypoint.sh` <br/> `wget {{site.url}}{% link /docker/entrypoint.sh %}`
|
||||
> 1. Download new `docker-compose.yml` (substitute `sqlite` with `postgres` in case): <br/>`wget {{site.url}}{% link /docker/sqlite/docker-compose.yml %}`
|
||||
> 1. Build the new container `docker-compose build`
|
||||
> 1. Extract your backup into `./data` <br/>`mkdir data; tar xvzf gancio-<yourLastBackup>-backup.tgz -C data`
|
||||
|
@ -96,4 +106,4 @@ tail -f data/logs/gancio.log
|
|||
```bash
|
||||
cd /opt/gancio
|
||||
docker-compose up -d --no-deps --build
|
||||
```
|
||||
```
|
||||
|
|
|
@ -6,17 +6,20 @@ has_children: true
|
|||
nav_order: 3
|
||||
has_toc: false
|
||||
---
|
||||
## Install
|
||||
## Pre-requisites
|
||||
- a Linux machine with <strong>root access</strong> (a VPS with 500MB of RAM and a cpu should be enough but do not use docker on a small machine :stuck_out_tongue_winking_eye:)
|
||||
- a domain name or subdomain (eg. gancio.mydomain.org, subpath are not supported)
|
||||
- an SMTP server to deliver emails
|
||||
|
||||
You can install gancio on a cheap VPS (500mb of ram will be enough)
|
||||
## Install
|
||||
|
||||
- [Install on Debian]({% link install/debian.md %})
|
||||
- [Install using docker]({% link install/docker.md %})
|
||||
|
||||
### Post installation
|
||||
- [Setup Nginx as a proxy]({% link install/nginx.md %})
|
||||
- [Configuration]({% link install/configuration.md %})
|
||||
- [Backup]({% link install/backup.md %})
|
||||
- [Setup a backup]({% link install/backup.md %})
|
||||
|
||||
|
||||
If you wanna hack or run the current development release take a look at [Hacking & contribute]({% link dev/dev.md %})
|
||||
> info "Info"
|
||||
> If you wanna hack or run the current development release take a look at [Hacking & contribute]({% link dev/dev.md %}).
|
||||
>
|
||||
|
|
|
@ -8,69 +8,35 @@ parent: Install
|
|||
|
||||
|
||||
## Nginx proxy configuration
|
||||
This is the default nginx configuration for gancio, please modify at least the **server_name** and **ssl_certificate**'s path.
|
||||
Note that this does not include a cache configuration and that gancio does
|
||||
not use a cache control at all, if you can help with this task you're
|
||||
welcome.
|
||||
This is the default nginx configuration for gancio, please modify at least «YOUR_DOMAIN». Note that it does not include HTTPS setup but you can easily use [certbot](https://certbot.eff.org/) for that.
|
||||
|
||||
- __You should be in the correct directory__
|
||||
`/etc/nginx/sites-available`
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name gancio.cisti.org;
|
||||
root /var/www/letsencrypt;
|
||||
location /.well-known/acme-challenge/ { allow all; }
|
||||
location / { return 301 https://$host$request_uri; }
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name gancio.cisti.org;
|
||||
|
||||
ssl_protocols TLSv1.2;
|
||||
ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
|
||||
# Uncomment these lines once you acquire a certificate:
|
||||
# ssl_certificate /etc/letsencrypt/live/gancio.cisti.org/fullchain.pem;
|
||||
# ssl_certificate_key /etc/letsencrypt/live/gancio.cisti.org/privkey.pem;
|
||||
server_name <<YOUR_DOMAIN>>;
|
||||
|
||||
keepalive_timeout 70;
|
||||
sendfile on;
|
||||
client_max_body_size 80m;
|
||||
|
||||
gzip on;
|
||||
gzip_disable "msie6";
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000";
|
||||
|
||||
location / {
|
||||
try_files $uri @proxy;
|
||||
}
|
||||
|
||||
location @proxy {
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header Proxy "";
|
||||
proxy_pass_header Server;
|
||||
|
||||
proxy_pass http://127.0.0.1:13120;
|
||||
proxy_buffering on;
|
||||
proxy_redirect off;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
tcp_nodelay on;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
- __Following this, you should create a link to the file in sites-enabled:__
|
||||
```bash
|
||||
ln -s /etc/nginx/sites-available/<your-config> /etc/nginx/sites-enabled/
|
||||
```
|
||||
|
|
|
@ -8,7 +8,7 @@ nav_order: 7
|
|||
|
||||
- [gancio.cisti.org](https://gancio.cisti.org) (Turin, Italy)
|
||||
- [lapunta.org](https://lapunta.org) (Florence, Italy)
|
||||
- [chesefa.org](https://chesefa.org) (Naples, Italy)
|
||||
- [termine.161.social](https://termine.161.social) (Germany)
|
||||
|
||||
|
||||
<small>Do you want your instance to appear here? [Write us]({% link contact.md %}).</small>
|
||||
<small>Do you want your instance to appear here? [Write us]({% link contact.md %}).</small>
|
||||
|
|
BIN
docs/latest.tgz
BIN
docs/latest.tgz
Binary file not shown.
|
@ -6,6 +6,6 @@ nav_order: 1
|
|||
has_children: true
|
||||
---
|
||||
|
||||
# Usage
|
||||
## Usage
|
||||
|
||||
ehmmm, help needed here :smile: feel free to send a PR => [here](https://framagit.org/les/gancio/tree/master/docs)
|
22
layouts/clean.vue
Normal file
22
layouts/clean.vue
Normal file
|
@ -0,0 +1,22 @@
|
|||
<template lang='pug'>
|
||||
v-app(app)
|
||||
Snackbar
|
||||
Confirm
|
||||
|
||||
v-main(app)
|
||||
v-fade-transition(hide-on-leave)
|
||||
nuxt
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import Snackbar from '../components/Snackbar'
|
||||
import Confirm from '../components/Confirm'
|
||||
|
||||
export default {
|
||||
name: 'Default',
|
||||
components: { Snackbar, Confirm },
|
||||
created () {
|
||||
this.$vuetify.theme.dark = false
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,10 +1,10 @@
|
|||
<template lang='pug'>
|
||||
v-app
|
||||
v-app(app)
|
||||
Snackbar
|
||||
Confirm
|
||||
Nav
|
||||
|
||||
v-main
|
||||
v-main(app)
|
||||
v-fade-transition(hide-on-leave)
|
||||
nuxt
|
||||
|
||||
|
@ -19,13 +19,18 @@ import Confirm from '../components/Confirm'
|
|||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
head () {
|
||||
return {
|
||||
htmlAttrs: {
|
||||
lang: this.locale
|
||||
}
|
||||
}
|
||||
},
|
||||
name: 'Default',
|
||||
components: { Nav, Snackbar, Footer, Confirm },
|
||||
computed: mapState(['settings']),
|
||||
computed: mapState(['settings', 'locale']),
|
||||
created () {
|
||||
this.$vuetify.theme.dark = this.settings['theme.is_dark']
|
||||
this.$vuetify.theme.themes.dark.primary = this.settings['theme.primary']
|
||||
this.$vuetify.theme.themes.light.primary = this.settings['theme.primary']
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
v-container.p-4.text-center
|
||||
v-alert(v-if="error.statusCode === 404") ¯\_(ツ)_/¯ {{error.message}}
|
||||
v-alert(v-else type='error') <v-icon>mdi-warning</v-icon> An error occurred: {{error.message}}
|
||||
nuxt-link(to='/') Back to home
|
||||
nuxt-link(to='/')
|
||||
v-btn Back to home
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
Nav
|
||||
|
||||
v-main(app)
|
||||
v-scroll-y-transition(hide-on-leave)
|
||||
v-fade-transition(hide-on-leave)
|
||||
nuxt
|
||||
|
||||
Footer
|
||||
|
|
|
@ -84,7 +84,9 @@
|
|||
"import": "Importa",
|
||||
"reset": "Reinicia",
|
||||
"theme": "Tema",
|
||||
"tags": "Etiquetes"
|
||||
"tags": "Etiquetes",
|
||||
"label": "Etiqueta",
|
||||
"max_events": "Nre. màx. d'activitats"
|
||||
},
|
||||
"login": {
|
||||
"description": "Amb la sessió iniciada pots afegir activitats noves.",
|
||||
|
@ -122,7 +124,7 @@
|
|||
"media_description": "Pots adjuntar un cartell (opcional)",
|
||||
"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 <b>prem Enter</b>. ",
|
||||
"where_description": "On es farà? Si no apareix el lloc el pots crear.",
|
||||
"confirmed": "S'ha confirmat l'activitat",
|
||||
"not_found": "No s'ha trobat l'activitat",
|
||||
"remove_confirmation": "Segur que vols esborrar l'activitat?",
|
||||
|
@ -153,14 +155,18 @@
|
|||
"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",
|
||||
"import_ICS": "Importa des d'un ICS",
|
||||
"import_URL": "Importa des d'una URL"
|
||||
"import_URL": "Importa des d'una URL",
|
||||
"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)",
|
||||
"edit_recurrent": "Edita l'activitat periòdica:",
|
||||
"updated": "S'ha actualitzat l'activitat"
|
||||
},
|
||||
"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.",
|
||||
"event_confirm_description": "Des d'aquí pots confirmar les activitats creades anònimament",
|
||||
"delete_user": "Esborra",
|
||||
"remove_admin": "Esborra admin",
|
||||
"delete_user_confirm": "Segur que vols esborrar aquest compte?",
|
||||
"delete_user_confirm": "Segur que vols esborrar el compte {user}?",
|
||||
"user_remove_ok": "S'ha esborrat el compte",
|
||||
"user_create_ok": "S'ha creat el compte",
|
||||
"allow_registration_description": "Vols deixar el registre obert?",
|
||||
|
@ -189,7 +195,7 @@
|
|||
"resources": "Recursos",
|
||||
"user_blocked": "L'usuari/a {user} ja no podrà afegir recursos",
|
||||
"favicon": "Logo",
|
||||
"user_block_confirm": "Segur/a que vols bloquejar l'usuària?",
|
||||
"user_block_confirm": "Segur que vols bloquejar a {user}?",
|
||||
"delete_announcement_confirm": "Segur/a que vols esborrar l'anunci?",
|
||||
"announcement_remove_ok": "S'ha esborrat l'anunci",
|
||||
"announcement_description": "En aquesta secció pots afegir anuncis que romandran a la pàgina principal",
|
||||
|
@ -210,7 +216,16 @@
|
|||
"delete_footer_link_confirm": "Segur que vols esborrar aquest enllaç?",
|
||||
"footer_links": "Enllaços del peu",
|
||||
"add_link": "Afegeix un enllaç",
|
||||
"is_dark": "Tema fosc"
|
||||
"is_dark": "Tema fosc",
|
||||
"disable_user_confirm": "Segur que vols deshabilitar a {user}?",
|
||||
"add_instance": "Afegeix una instància",
|
||||
"instance_block_confirm": "Segur que vols bloquejar la instància {instance}?",
|
||||
"show_smtp_setup": "Configuració de correu",
|
||||
"smtp_hostname": "Amfitrió SMTP (hostname)",
|
||||
"smtp_description": "<ul><li>L'admin hauria de rebre un correu cada cop que es pengi alguna una activitat anònima (si estan activades).</li><li>L'admin hauria de rebre un correu per cada soŀlicitud de registre (si estan actives).</li><li>La usuària hauria de rebre un correu després de soŀlicitar registrar-se.</li><li>La usuària hauria de rebre un correu quan se li hagi confirmat el registre.</li><li>La usuària hauria de rebre un correu si l'admin la registra directament.</li><li>La usuària hauria de rebre un correu de restabliment de contrasenya si ho demana</li></ul>",
|
||||
"smtp_test_success": "S'ha enviat un correu de prova a {admin_email}, comprova que hagi arribat bé",
|
||||
"smtp_test_button": "Envia un correu de prova",
|
||||
"admin_email": "Correu d'admin"
|
||||
},
|
||||
"auth": {
|
||||
"not_confirmed": "Encara no s'ha confirmat…",
|
||||
|
@ -252,5 +267,11 @@
|
|||
"validators": {
|
||||
"email": "Escriu una adreça de correu vàlida",
|
||||
"required": "Cal omplir el camp {fieldName} és"
|
||||
},
|
||||
"setup": {
|
||||
"completed": "S'ha completat la configuració inicial",
|
||||
"check_db": "Comprova la BD",
|
||||
"completed_description": "<p>Ara ja pots entrar amb aquesta usuària:<br/><br/>Nom: <b>{email}</b><br/>Contrasenya: <b>{password}<b/></p>",
|
||||
"start": "Comença"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"register": {
|
||||
"subject": "Hem rebut una soŀlicitud de registre",
|
||||
"content": "Hem rebut una soŀlicitud de registre. Hi respondrem tan aviat com ens sigui possible.\nSalut"
|
||||
"content": "Hem rebut una soŀlicitud de registre. Hi respondrem tan aviat com ens sigui possible."
|
||||
},
|
||||
"confirm": {
|
||||
"subject": "Ja pots publicar activitats",
|
||||
|
@ -18,5 +18,12 @@
|
|||
"admin_register": {
|
||||
"subject": "Registre nou",
|
||||
"content": "{{user.email}} ha soŀlicitat regsitrar-se a {{config.title}}: <br/><pre>{{user.description}}</pre><br/> Respon a la soŀlicitud <a href='{{config.baseurl}}/admin'>aquí</a>."
|
||||
},
|
||||
"event_confirm": {
|
||||
"content": "Pots acceptar aquesta activitat a <a href='{{url}}'>la pàgina de confirmació</a>"
|
||||
},
|
||||
"test": {
|
||||
"subject": "La configuració SMTP funciona",
|
||||
"content": "Aquest és un correu de prova, si llegeixes això és que la configuració funciona."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,5 +18,12 @@
|
|||
"admin_register": {
|
||||
"subject": "New registration",
|
||||
"content": "{{user.email}} has requested registration on {{config.title}}: <br/><pre>{{user.description}}</pre><br/> Confirm it <a href='{{config.baseurl}}/admin'>here</a>."
|
||||
},
|
||||
"event_confirm": {
|
||||
"content": "You can confirm this event at <a href='{{url}}'>this page</a>"
|
||||
},
|
||||
"test": {
|
||||
"subject": "Your SMTP configuration is working",
|
||||
"content": "This is a test email, if you are reading this your configuration is working."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"register": {
|
||||
"subject": "Solicitud de registro recibida",
|
||||
"content": "Recibimos la solicitud de registro. Lo confirmaremos tan pronto como podamos.\n Adios"
|
||||
"content": "Recibimos la solicitud de registro. Lo confirmaremos tan pronto como podamos."
|
||||
},
|
||||
"confirm": {
|
||||
"subject": "Puedes empezar a publicar eventos",
|
||||
|
@ -21,5 +21,8 @@
|
|||
"admin_register": {
|
||||
"subject": "Nuevo registro",
|
||||
"content": "{{user.email}} ha pedido registrarse en {{config.title}}: <br/><pre>{{user.description}}</pre><br/> Confírmalo <a href='{{config.baseurl}}/admin'>aquí</a>."
|
||||
},
|
||||
"event_confirm": {
|
||||
"content": "Puede confirmar este evento <a href='{{url}}'>aquí</a>"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,5 +18,12 @@
|
|||
"register": {
|
||||
"content": "Nous avons reçu la demande d'inscription. Nous la confirmerons au plus vite.",
|
||||
"subject": "Demande d'inscription reçue"
|
||||
},
|
||||
"event_confirm": {
|
||||
"content": "Vous pouvez confirmer cet événement sur <a href='{{url}}'>cette page</a>"
|
||||
},
|
||||
"test": {
|
||||
"subject": "Votre configuration SMTP est fonctionnelle",
|
||||
"content": "Ceci est un e-mail de test, si vous pouvez lire ceci c'est que votre configuration est fonctionnelle."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,5 +18,8 @@
|
|||
"admin_register": {
|
||||
"subject": "Nuova registrazione",
|
||||
"content": "{{user.email}} si è registratǝ a {{config.title}} scrivendo:<br/><pre>{{user.description}}</pre><br/> Puoi confermarlo <a href='{{config.baseurl}}/admin'>qui</a>."
|
||||
},
|
||||
"event_confirm": {
|
||||
"content": "Puoi confermare questo evento premendo il tasto conferma in <a href='{{url}}'>questa pagina</a>"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,8 +45,8 @@
|
|||
"new_user": "New user",
|
||||
"ok": "Ok",
|
||||
"cancel": "Cancel",
|
||||
"enable": "Turn on",
|
||||
"disable": "Turn off",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable",
|
||||
"me": "You",
|
||||
"password_updated": "Password changed.",
|
||||
"resources": "Resources",
|
||||
|
@ -123,6 +123,7 @@
|
|||
"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.",
|
||||
|
@ -165,7 +166,8 @@
|
|||
"event_confirm_description": "You can confirm events entered by anonymous users here",
|
||||
"delete_user": "Remove",
|
||||
"remove_admin": "Remove admin",
|
||||
"delete_user_confirm": "Are you sure you want to remove this user?",
|
||||
"disable_user_confirm": "Are you sure you want to disable {user}?",
|
||||
"delete_user_confirm": "Are you sure you want to remove {user}?",
|
||||
"user_remove_ok": "User removed",
|
||||
"user_create_ok": "User created",
|
||||
"allow_registration_description": "Allow open registrations?",
|
||||
|
@ -195,7 +197,8 @@
|
|||
"resources": "Resources",
|
||||
"user_blocked": "User {user} blocked",
|
||||
"favicon": "Logo",
|
||||
"user_block_confirm": "Are you sure you want block this user?",
|
||||
"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",
|
||||
|
@ -216,7 +219,14 @@
|
|||
"footer_links": "Footer links",
|
||||
"delete_footer_link_confirm": "Sure to remove this link?",
|
||||
"edit_place": "Edit place",
|
||||
"new_announcement": "New announcement"
|
||||
"new_announcement": "New announcement",
|
||||
"show_smtp_setup": "Email settings",
|
||||
"smtp_hostname": "SMTP Hostname",
|
||||
"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",
|
||||
"admin_email": "Admin e-mail"
|
||||
|
||||
},
|
||||
"auth": {
|
||||
"not_confirmed": "Not confirmed yet…",
|
||||
|
@ -258,5 +268,10 @@
|
|||
"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>",
|
||||
"start": "Start"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
"ok": "Ok",
|
||||
"cancel": "Cancelar",
|
||||
"enable": "Habilitar",
|
||||
"disable": "Deshabilita",
|
||||
"disable": "Deshabilitar",
|
||||
"me": "Tú",
|
||||
"password_updated": "Contraseña actualizada.",
|
||||
"comments": "ningún comentario|un comentario|{n} comentarios",
|
||||
|
@ -85,7 +85,9 @@
|
|||
"tags": "Tags",
|
||||
"import": "Importar",
|
||||
"reset": "Reset",
|
||||
"theme": "Tema"
|
||||
"theme": "Tema",
|
||||
"label": "Etiqueta",
|
||||
"max_events": "Número de eventos máximo"
|
||||
},
|
||||
"login": {
|
||||
"description": "Entrando podrás publicar nuevos eventos.",
|
||||
|
@ -103,7 +105,7 @@
|
|||
"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.",
|
||||
"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, utilizás una aplicación especial para recibir actualizaciones de los sitios que más te interesan, como por ejemplo este. 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/>\n Agregando este link a tu lector de feed, estarás siempre actualizado/a.",
|
||||
"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/>\n Agregando este link a tu lector de feed, estarás siempre actualizado/a.",
|
||||
"ical_description": "Las computadoras y los teléfonos inteligentes suelen estar equipados con una aplicación para administrar un calendario. Estos programas generalmente se pueden usar para importar un calendario remoto.",
|
||||
"list_description": "Si tienes un sitio web y quieres mostrar una lista de eventos, puedes usar el siguiente código"
|
||||
},
|
||||
|
@ -120,7 +122,7 @@
|
|||
"what_description": "Nombre evento",
|
||||
"description_description": "Descripción, puedes copiar y pegar",
|
||||
"tag_description": "Tag...",
|
||||
"media_description": "Puedes agregar un panfleto (opcionál)",
|
||||
"media_description": "Puedes agregar una imagen (opcional)",
|
||||
"added": "Evento agregado",
|
||||
"added_anon": "Evento agregado, será confirmado cuanto antes.",
|
||||
"where_description": "¿Dónde es? Si el lugar no está, escribilo.",
|
||||
|
@ -128,8 +130,8 @@
|
|||
"not_found": "Evento no encontrado",
|
||||
"remove_confirmation": "¿Estás seguro/a de querér eliminar este evento?",
|
||||
"recurrent": "Recurrente",
|
||||
"recurrent_description": "Elegí la frecuencia y selecciona los días.",
|
||||
"multidate_description": "¿Un festival o más de un día? Elegí cuándo comienza y cuándo termina.",
|
||||
"recurrent_description": "Elegí la frecuencia y selecciona los días",
|
||||
"multidate_description": "¿Un festival o más de un día? Elegí cuándo comienza y cuándo termina",
|
||||
"multidate": "Más días",
|
||||
"normal": "Normal",
|
||||
"normal_description": "Selecciona el día.",
|
||||
|
@ -154,14 +156,18 @@
|
|||
"ics": "ICS",
|
||||
"import_ICS": "Importar desde ICS",
|
||||
"import_URL": "Importar desde la URL",
|
||||
"only_future": "solo eventos venideros"
|
||||
"only_future": "solo eventos venideros",
|
||||
"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"
|
||||
},
|
||||
"admin": {
|
||||
"place_description": "En el caso de que un lugar sea incorrecto o cambie de dirección, puedes cambiarlo. <br/> En este caso hay que tener en cuenta que todos los eventos asociados con ese lugar cambiarán de dirección (¡incluso los pasados!)",
|
||||
"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.",
|
||||
"event_confirm_description": "Puedes confirmar aquí los eventos agregados por usuarios anónimos",
|
||||
"delete_user": "Elimina",
|
||||
"remove_admin": "Borra admin",
|
||||
"delete_user_confirm": "¿Estás seguro/a de borrar este usuario?",
|
||||
"delete_user_confirm": "¿Estás seguro/a de borrar a {user}?",
|
||||
"user_remove_ok": "Usuario eliminado",
|
||||
"user_create_ok": "Usuario creado",
|
||||
"allow_registration_description": "¿Querés habilitar el registro?",
|
||||
|
@ -170,7 +176,7 @@
|
|||
"allow_recurrent_event": "Habilitar eventos fijos",
|
||||
"recurrent_event_visible": "Eventos fijos visibles por defecto",
|
||||
"federation": "Federación / ActivityPub",
|
||||
"enable_federation": "Habilitar la federación!",
|
||||
"enable_federation": "Habilitar la federación",
|
||||
"enable_federation_help": "Será posible seguir esta instancia desde el fediverso",
|
||||
"select_instance_timezone": "Uso horario",
|
||||
"enable_resources": "Habilitar recursos",
|
||||
|
@ -179,7 +185,7 @@
|
|||
"hide_boost_bookmark_help": "Oculta los pequeños iconos que muestran el número de impulsos y marcadores que vienen del fediverso",
|
||||
"block": "Bloquear",
|
||||
"unblock": "Desbloquear",
|
||||
"user_add_help": "Enviaremos un correo electrónico al nuevo usuario con instrucciones para confirmar la suscripción y elegir una contraseña.",
|
||||
"user_add_help": "Enviaremos un correo electrónico al nuevo usuario con instrucciones para confirmar la suscripción y elegir una contraseña",
|
||||
"instance_name": "Nombre de la instancia",
|
||||
"show_resource": "Mostrar recurso",
|
||||
"hide_resource": "Ocultar recurso",
|
||||
|
@ -191,7 +197,7 @@
|
|||
"resources": "Recursos",
|
||||
"user_blocked": "El usuario {usuario} ya no podrá añadir recursos",
|
||||
"favicon": "Logo",
|
||||
"user_block_confirm": "¿Estás seguro de que quieres bloquear al usuario?",
|
||||
"user_block_confirm": "¿Estás seguro de que quieres bloquear a {user}?",
|
||||
"delete_announcement_confirm": "¿Estás seguro de que quieres borrar el anuncio?",
|
||||
"announcement_remove_ok": "Anuncio borrado",
|
||||
"announcement_description": "En esta sección se pueden insertar anuncios que permanecerán en la página de inicio",
|
||||
|
@ -212,7 +218,10 @@
|
|||
"delete_footer_link_confirm": "Seguro que quieres quitar este enlace?",
|
||||
"footer_links": "Enlaces a pie de página",
|
||||
"add_link": "Añadir enlace",
|
||||
"is_dark": "Tema oscuro"
|
||||
"is_dark": "Tema oscuro",
|
||||
"instance_block_confirm": "¿Estás seguro/a que quieres bloquear la instancia {instance}?",
|
||||
"add_instance": "Añadir instancia",
|
||||
"disable_user_confirm": "Estas seguro de que quieres deshabilitar a {user}?"
|
||||
},
|
||||
"auth": {
|
||||
"not_confirmed": "Todavía no hemos confirmado este email…",
|
||||
|
@ -227,7 +236,7 @@
|
|||
"update_confirm": "¿Estás seguro de que quieres guardar los cambios?"
|
||||
},
|
||||
"error": {
|
||||
"nick_taken": "Este nickname ya está registrado",
|
||||
"nick_taken": "Este apodo ya está registrado.",
|
||||
"email_taken": "Este correo electrónico ya está registrado."
|
||||
},
|
||||
"ordinal": {
|
||||
|
@ -238,11 +247,11 @@
|
|||
"5": "quinto",
|
||||
"-1": "último"
|
||||
},
|
||||
"about": "\n <p>\n Gancio es un proyecto del <a href='https://autistici.org/underscore'>underscore hacklab</a> y es uno de los\n servicios de <a href='https://cisti.org'>cisti.org</a>.</p>\n\n <h5>¿Que es gancio?</h5>\n <p>Gancio (se pronuncia \"gancho\") es una herramienta para compartir eventos orientado a las comunidades radicales.\n Dentro del gancio pueden encontrar y agregar eventos.\n Gancio, como todo <a href='https://cisti.org'> cisti.org </a> es una herramienta\n antisexista, antirracista, antifascista y anticapitalista, así que piensen en eso cuando\n van a publicar un evento. </p>\n\n <h5>Ok, pero ¿que quiere decir gancio?</h5>\n <p>\n Literalmente sería \"enganche\", pero en realidad viene de una forma de decir que se usa en en Turín (Italia). Ahí si alguien dice: \"ehi, ci diamo un gancio alle 8?\" (\"ehi, ¿nos damos un enganche a las 8?\") quiere decir \"ehí, ¿nos vemos a las 8?\". \"Darsi un gancio\" es juntarse a una hora X en un lugar Y.</p>\n <code>\n <ul>\n <li> ¿A qué hora es el <i>gancio</i> para ir a la marcha?</li>\n <li> No sé, de todos modos no puedo ir, ya tengo un <i>gancio</i> para ir a una reunión.</li>\n </ul>\n </code>\n\n <h5> Contactos</h5>\n <p>\n ¿Escribiste una nueva interfaz para gancio? ¿Quieres abrir un gancio en tu ciudad?\n ¿Hay algo que te gustaría mejorar? Para contribuir el código fuente es libre y disponible \n <a href='https://git.lattuga.net/cisti/gancio'>aquí</a>. Ayuda y sugerencias son siempre bienvenidos, puedes comunicarte con nosotros \n enviando un mail a underscore arroba autistici.org</p>",
|
||||
"about": "\n <p><a href='https://gancio.org'>Gancio</a> es una agenda compartida para comunidades locales.</p>\n ",
|
||||
"confirm": {
|
||||
"title": "Confirmación de usuario",
|
||||
"not_valid": "Mmmmm algo salió mal.",
|
||||
"valid": "Su cuenta ha sido confirmada, ahora puede <a href=\"/login\">ingresar</a>."
|
||||
"valid": "Su cuenta ha sido confirmada, ahora puede <a href=\"/login\">ingresar</a>"
|
||||
},
|
||||
"oauth": {
|
||||
"authorization_request": "La aplicación externa <code>{app}</code> requiere permiso para realizar las siguientes tareas en <code>{instance_name}</code>:",
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
"enable": "Gaitu",
|
||||
"disable": "Desgaitu",
|
||||
"me": "Zu",
|
||||
"password_updated": "Pasahitza eguneratuta!",
|
||||
"password_updated": "Pasahitza eguneratuta.",
|
||||
"activate_user": "Egiaztatuta",
|
||||
"displayname": "Erakutsitako izena",
|
||||
"federation": "Federazioa",
|
||||
|
@ -80,37 +80,43 @@
|
|||
"delete": "Ezabatu",
|
||||
"announcements": "Iragarkiak",
|
||||
"url": "URL esteka",
|
||||
"place": "Lekua"
|
||||
"place": "Lekua",
|
||||
"label": "Etiketa",
|
||||
"max_events": "Max zenbakidun gertaerak",
|
||||
"import": "Inportatu",
|
||||
"reset": "Zeroan jarri",
|
||||
"theme": "Gai",
|
||||
"tags": "Tags"
|
||||
},
|
||||
"login": {
|
||||
"description": "Saioa hasiz gero, ekitaldi berriak sortu ahal izango dituzu",
|
||||
"check_email": "Begiratu zure postontzi elektronikoan, baita mezu baztergarrietan",
|
||||
"description": "Saioa hasiz gero, ekitaldi berriak sortu ahal izango dituzu.",
|
||||
"check_email": "Begiratu zure postontzi elektronikoan, baita mezu baztergarrietan.",
|
||||
"not_registered": "Ez duzu izena eman?",
|
||||
"forgot_password": "Pasahitza ahaztu duzu?",
|
||||
"error": "Ezin da saioa hasi, egiaztatu zure datuok.",
|
||||
"insert_email": "Sartu zure helbide elektronikoa",
|
||||
"ok": "Saioa hasi duzu!"
|
||||
"ok": "Saioa hasi duzu"
|
||||
},
|
||||
"recover": {
|
||||
"not_valid_code": "Mmmmm zerbaitek huts egin du..."
|
||||
},
|
||||
"export": {
|
||||
"intro": "Kapitalismoaren plataformek edozer egingo dute erabiltzaileak eta haien datuak gordetzeko. Guk aldiz, informazioak, pertsonen antzera askeak izan behar dutela sinesten dugu. Horretarako gogoko dituzun ekitaldietaz info eguneratuak jaso ditzakezu webgune honetatik pasatzeko beharrik gabe.",
|
||||
"email_description": "Interesatzen zaizkizun ekitaldiak jaso ditzakezu posta elektronikoan",
|
||||
"intro": "Kapitalismoaren plataformek edozer egingo dute erabiltzaileak eta haien datuak gordetzeko. Guk aldiz, informazioak, pertsonen antzera askeak izan behar dutela sinesten dugu. Horretarako gogoko dituzun ekitaldietaz info eguneratuak jaso ditzakezu webgune honetatik pasatzeko beharrik gabe.",
|
||||
"email_description": "Interesatzen zaizkizun ekitaldiak jaso ditzakezu posta elektronikoan.",
|
||||
"insert_your_address": "Sartu zure helbide elektronikoa",
|
||||
"feed_description": "Eguneraketak sakelekoan edo ordenagailuan jaso nahi badituzu webgune hau bisitatu gabe, RSS jarioa erabiltzea gomendatzen dizugu.</p>\n<p>RSS jarioarentzat aplikazio berezi bat erabiliko duzu gogoko dituzun weguneetatik berriak jasotzeko. Oso modu egokia da gune askotako berriak erraz eta azkar jasotzeko eta ez da konturik sortu behar! </p>\n\n<li>Android baldin badaukazu <a href=\"https://play.google.com/store/apps/details?id=net.frju.flym\">Flym</a> edo Feeder gomendatzen dizugu</li>\n<li>iPhone/iPad-erako eskuragarri daukazu <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\">Feed4U</a></li>\n<li>Ordenagailuaren kasuan Feedbro iradokitzen dugu, <a href=\"https://addons.mozilla.org/en-GB/firefox/addon/feedbroreader/\">Firefoxeko</a> edo <a href=\"https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa\">Chromeko</a> gehigarri gisa instalatzen da eta sistema gehienetan dabil.</li>\n<br/>\nHonako esteka jario irakurgailuan sartuta, eguneraketa guztiak jasoko dituzu.",
|
||||
"ical_description": "Normalean ordenagailuak eta smartphoneak egutegiak inportatu eta kudeatzeko aplikazioekin etorri ohi dira",
|
||||
"list_description": "Webgune bat baduzu eta ekitaldien zerrenda erakutsi nahi baduzu, ondorengo kodea erabili dezakezu"
|
||||
"feed_description": "Eguneraketak sakelekoan edo ordenagailuan jaso nahi badituzu webgune hau bisitatu gabe, RSS jarioa erabiltzea gomendatzen dizugu.</p>\n\n<p>RSS jarioarentzat aplikazio berezi bat erabiliko duzu gogoko dituzun weguneetatik berriak jasotzeko. Oso modu egokia da gune askotako berriak erraz eta azkar jasotzeko eta ez da konturik sortu behar! </p>\n\n<li>Android baldin badaukazu <a href=\"https://play.google.com/store/apps/details?id=net.frju.flym\">Flym</a> edo Feeder gomendatzen dizugu</li>\n<li>iPhone/iPad-erako eskuragarri daukazu <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\">Feed4U</a></li>\n<li>Ordenagailuaren kasuan Feedbro iradokitzen dugu, <a href=\"https://addons.mozilla.org/en-GB/firefox/addon/feedbroreader/\">Firefoxeko</a> edo <a href=\"https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa\">Chromeko</a> gehigarri gisa instalatzen da eta sistema gehienetan dabil.</li>\n<br/>\nHonako esteka jario irakurgailuan sartuta, eguneraketa guztiak jasoko dituzu.",
|
||||
"ical_description": "Normalean ordenagailuak eta smartphoneak egutegiak inportatu eta kudeatzeko aplikazioekin etorri ohi dira.",
|
||||
"list_description": "Webgune bat baduzu eta ekitaldien zerrenda erakutsi nahi baduzu, ondorengo kodea erabili dezakezu"
|
||||
},
|
||||
"register": {
|
||||
"description": "Herri mugimenduek autoantolaketaren bidean diru-iturrien beharrak dauzkatela badakigu.<br/>Honako hauxe oparitxoa da, hortaz erabili ezazue ekitaldi ez-komertzialak iragartzeko, eta esan gabe doa, ekitaldi antifaxistak, antisexistak eta antiarriztetarako :) . \n<br/>Argitaratzen hasi baino lehen<strong> zure kontu berriak onarpena jaso beharko du </strong>beraz, <strong>webgune honen atzean hezur-haragizko pertsonak gaudela jakinda </strong>, (momenutz euskal 'AI'-rik ez daukagu baina adi, agertuko direla) idatzi iezaguzu lerro batzuk argitaratu nahi dituzun ekitaldiei buruz",
|
||||
"description": "Herri mugimenduek autoantolaketaren bidean diru-iturrien beharrak dauzkatela badakigu.<br/>Honako hauxe oparitxoa da, hortaz erabili ezazue ekitaldi ez-komertzialak iragartzeko, eta esan gabe doa, ekitaldi antifaxistak, antisexistak eta antiarriztetarako :) .\n<br/>Argitaratzen hasi baino lehen<strong> zure kontu berriak onarpena jaso beharko du </strong>beraz, <strong>webgune honen atzean hezur-haragizko pertsonak gaudela jakinda </strong>, (momenutz euskal 'AI'-rik ez daukagu baina adi, agertuko direla) idatzi iezaguzu lerro batzuk argitaratu nahi dituzun ekitaldiei buruz.",
|
||||
"error": "Hutsa: ",
|
||||
"complete": "Izen-ematea baieztatu behar dute.",
|
||||
"first_user": "Administratzailea sortu da"
|
||||
},
|
||||
"event": {
|
||||
"anon": "Ezezaguna",
|
||||
"anon_description": "Ekitaldia sortu dezakezu <a href='/login'>saioa hasi</a> edo <a href='/register'>izena eman</a> gabe,\nbaina kasu honetan norbaitek egiaztatu beharko du ekitaldia gune honetarako egokia dela eta itxaron beharko duzu. Gainera, behin egiaztatuta hura aldatzea ez da posiblea izango.<br/><br/>\n Dena den, ahalik eta azkarren erantzuten saiatuko gara.",
|
||||
"anon_description": "Ekitaldia sortu dezakezu <a href='/login'>saioa hasi</a> edo <a href='/register'>izena eman</a> gabe,\nbaina kasu honetan norbaitek egiaztatu beharko du ekitaldia gune honetarako egokia dela eta itxaron beharko duzu. Gainera, behin egiaztatuta hura aldatzea ez da posiblea izango.<br/><br/>\nDena den, ahalik eta azkarren erantzuten saiatuko gara. ",
|
||||
"same_day": "egun berean",
|
||||
"what_description": "Ekitaldiaren izena",
|
||||
"description_description": "Ekitaldiaren azalpena",
|
||||
|
@ -118,16 +124,16 @@
|
|||
"media_description": "Eskuorria edo irudia gehitu dezakezu (aukerakoa)",
|
||||
"added": "Ekitaldia sortu da",
|
||||
"added_anon": "Ekitaldia sortu da, baina baieztatzear dago.",
|
||||
"where_description": "Non da ekitaldia? Lekua ez bada zerrendan agertzen idatzi ezazu eta <b>enter sakatu</b>. ",
|
||||
"where_description": "Non da ekitaldia? Lekua ez bada zerrendan agertzen idatzi ezazu eta <b>enter sakatu</b>.",
|
||||
"confirmed": "Ekitaldia egiaztatu da",
|
||||
"not_found": "Ezin da ekitaldia aurkitu",
|
||||
"remove_confirmation": "Ziur zaude ekitaldi hau ezabatu nahi duzula?",
|
||||
"remove_recurrent_confirmation": "Ziur zaude ekitaldi errepikari hau ezabatu nahi duzula??\n\nIragan diren ekitaldiak mantenduko dira, baina ez da ekitaldi berririk sortuko.",
|
||||
"remove_recurrent_confirmation": "Ziur zaude ekitaldi errepikari hau ezabatu nahi duzula?\nIragan diren ekitaldiak mantenduko dira, baina ez da ekitaldi berririk sortuko.",
|
||||
"recurrent": "Errepikaria",
|
||||
"show_recurrent": "Ekitaldi errepikariak",
|
||||
"show_past": "Erakutsi iraganeko ekitaldiak",
|
||||
"recurrent_description": "Aukera ezazu maiztasuna eta hautatu egunak",
|
||||
"multidate_description": "Egun bat baino gehiagoko jaialdia da? Aukeratu noiz hasten den eta noiz amaitzen den.",
|
||||
"multidate_description": "Egun bat baino gehiagoko jaialdia da? Aukeratu noiz hasten den eta noiz amaitzen den",
|
||||
"multidate": "Egun gehiagotan",
|
||||
"normal": "Egunekoa",
|
||||
"normal_description": "Eguna aukeratu.",
|
||||
|
@ -143,27 +149,36 @@
|
|||
"due": "Amaiera ordua",
|
||||
"from": "Hasiera ordua",
|
||||
"image_too_big": "Irudia handiegia omen da (4mb gehienez)",
|
||||
"interact_with_me": "Elkar gaitezen fedibertsoan: ",
|
||||
"follow_me_description": " {title}n argitaratutako ekitaldien berri izateko aukeren artean,\n fedibertsoko <u>{account}</u> kontuari jarraitzea daukazu. Horretarako Mastodon erabili dezakezu, eta bertatik baliabideak gehitu ekitaldi baten.<br/><br/>\n Mastodon eta Fedibertsoa zer diren ez badakizu <a href='https://es.wikipedia.org/wiki/Fediverso'>artikulu hau</a> irakurtzea iradokitzen dizugu.<br/><br/> Sartu zure instantzia behean (adibidez mastodon.eus edo mastodon.jalgi.eus)"
|
||||
"interact_with_me": "Elkar gaitezen fedibertsoan",
|
||||
"follow_me_description": "{title}n argitaratutako ekitaldien berri izateko aukeren artean,\n fedibertsoko <u>{account}</u> kontuari jarraitzea daukazu. Horretarako Mastodon erabili dezakezu, eta bertatik baliabideak gehitu ekitaldi baten.<br/><br/>\n Mastodon eta Fedibertsoa zer diren ez badakizu <a href='https://es.wikipedia.org/wiki/Fediverso'>artikulu hau</a> irakurtzea iradokitzen dizugu.<br/><br/> Sartu zure instantzia behean (adibidez mastodon.eus edo mastodon.jalgi.eus)",
|
||||
"import_description": "Beste plataforma eta adibide batzuetako gertaerak formatu estandarren bidez inportatu ditzakezu (ics eta h-event)",
|
||||
"ics": "ICS",
|
||||
"import_ICS": "ICS-ko inportazioa",
|
||||
"import_URL": "URL-ko inportazioa",
|
||||
"interact_with_me_at": "Hitz egin nirekin fediversoan",
|
||||
"only_future": "gertakizunak besterik ez",
|
||||
"edit_recurrent": "Gertaera errepikakorra:",
|
||||
"updated": "Gertaera eguneratua",
|
||||
"saved": "Gertaera salbatua"
|
||||
},
|
||||
"admin": {
|
||||
"place_description": "Lekuaren zehaztapenak aldatu ditzakezu, bai gaizki idatzita dagoelako, bai helbidez aldatu delako.<br/> Ondorioz, leku horrekin lotutako ekitaldi guztiak helbidez aldatuko direla kontuan hartu behar da (baita iraganekoak ere!)",
|
||||
"event_confirm_description": "Erabiltzaile ezezagunek sortutako ekitaldiak hemen egiaztatu ditzakezu",
|
||||
"delete_user": "Erabiltzailea ezabatu",
|
||||
"remove_admin": "Administratzailea ezabatu",
|
||||
"delete_user_confirm": "Ziur zaude erabiltzailea ezabatu nahi duzula?",
|
||||
"delete_user_confirm": "Ziur zaude {user} ezabatu nahi duzula?",
|
||||
"user_remove_ok": "Erabiltzailea ezabatu da",
|
||||
"user_create_ok": "Erabiltzailea sortu da",
|
||||
"allow_registration_description": "Izen-emateak ahalbidetu nahi dituzu?",
|
||||
"allow_anon_event": "Ezezagunek ekitaldiak sortzea ahalbidetu nahi duzu? (Beti ere baieztapenarekin) ",
|
||||
"allow_recurrent_event": "Ekitaldi errepikariak ahalbidetu?",
|
||||
"allow_anon_event": "Ezezagunek ekitaldiak sortzea ahalbidetu nahi duzu? (Beti ere baieztapenarekin)",
|
||||
"allow_recurrent_event": "Ekitaldi errepikariak ahalbidetu",
|
||||
"recurrent_event_visible": "Erakutsi ekitaldi errepikariak modu lehenetsian",
|
||||
"federation": "Federazioa / ActivityPub",
|
||||
"enable_federation": "Federatzea gaitu",
|
||||
"enable_federation_help": "Instantzia hau fedibertsoan jarraitzea gaituko duzu?",
|
||||
"enable_federation_help": "Instantzia hau fedibertsoan jarraitzea gaituko duzu",
|
||||
"select_instance_timezone": "Ordu-eremua",
|
||||
"instance_timezone_description": "Gancio hiri baten moduko lekuen ekitaldiak biltzeko diseinatuta dago. Leku honen ordu-eremua hautatuz gero ekitaldi gutziek ordu-eremu horrekiko adieraziko dira..",
|
||||
"enable_resources": "Baliabideak gaitu ",
|
||||
"enable_resources": "Baliabideak gaitu",
|
||||
"enable_resources_help": "Fedibertsotik ekitaldietan baliabideak gehitzea ahalbidetzen du",
|
||||
"hide_boost_bookmark": "Bultzadak eta laster-markak ezkutatu",
|
||||
"hide_boost_bookmark_help": "Fedibertsotik datozen bultzaden eta laster-marken ikonotxoak ezkutatzen ditu",
|
||||
|
@ -181,37 +196,46 @@
|
|||
"filter_users": "Erabiltzaileak iragazi",
|
||||
"instance_name": "Instantziaren izena",
|
||||
"favicon": "Iruditxoa",
|
||||
"user_block_confirm": "Ziur zaude erabiltzailea blokeatu nahi duzula?",
|
||||
"user_block_confirm": "Ziur zaude {user} blokeatu nahi duzula?",
|
||||
"delete_announcement_confirm": "Ziur zaude iragarkia ezabatu nahi duzula?",
|
||||
"announcement_remove_ok": "Iragarkia ezabatu da",
|
||||
"announcement_description": "Atal honetan iragarkiak txertatu ditzakezu hasiera-orrian ager daitezen",
|
||||
"instance_locale": "Instantziaren hizkuntza lehenetsia",
|
||||
"instance_locale_description": "Orriak erakusteko erabilitako hizkuntza erabiltzaileak nahiago duen hizkuntza da. Hala ere, kasu batzuetan mezuak modu berean erakutsi behar ditugu guztiontzat (adibidez ActivityPub-etik argitaratzen dugunean edo posta elektroniko batzuk bidaltzerakoan). Kasu hauetan goian hautatutako hizkuntza erabiliko dugu.",
|
||||
"instance_place": "Instantziaren kokalekua ",
|
||||
"title_description": "Orriaren izenburuan, jario eta ics-en esportazioan eta mezu elektronikoen gaian erabiliko da ",
|
||||
"instance_place": "Instantziaren kokalekua",
|
||||
"title_description": "Orriaren izenburuan, jario eta ics-en esportazioan eta mezu elektronikoen gaian erabiliko da.",
|
||||
"description_description": "Orriburuan agertuko da, izenburuarekin batera",
|
||||
"instance_name_help": "Instantziaren kontua ActivityPub-en ",
|
||||
"instance_name_help": "Instantziaren kontua ActivityPub-en",
|
||||
"enable_trusted_instances": "Kideko instantziak gaitu",
|
||||
"trusted_instances_help": "Kideko instantzien zerrenda orri-buruan agertuko dira",
|
||||
"add_trusted_instance": "Gehitu kideko instantzia bat",
|
||||
"instance_place_help": "Beste instantzien zerrendetan agertuko den izena ",
|
||||
"delete_trusted_instance_confirm": "Ziur zaude kideko instantzia hau zerrendatik ezabatu nahi duzula?"
|
||||
"instance_place_help": "Beste instantzien zerrendetan agertuko den izena",
|
||||
"delete_trusted_instance_confirm": "Ziur zaude kideko instantzia hau zerrendatik ezabatu nahi duzula?",
|
||||
"new_announcement": "Iragarpen berria",
|
||||
"edit_place": "Leku ederrean",
|
||||
"delete_footer_link_confirm": "Ziur lotura kenduko duzula?",
|
||||
"footer_links": "Oinezkoen konexioak",
|
||||
"add_link": "Gehitu lotura",
|
||||
"is_dark": "Gai iluna",
|
||||
"instance_block_confirm": "Ziur al zaude blokearen adibidea {instance} nahi duzula?",
|
||||
"add_instance": "Gehitu adibidea",
|
||||
"disable_user_confirm": "Ziur zaude {user} deskonektatu nahi duzula?"
|
||||
},
|
||||
"auth": {
|
||||
"not_confirmed": "Oraindik baieztatu gabe dago...",
|
||||
"not_confirmed": "Oraindik baieztatu gabe dago…",
|
||||
"fail": "Saioa hasteak huts egin du! Ziur zaude datuok ondo daudela?"
|
||||
},
|
||||
"settings": {
|
||||
"update_confirm": "Aldaketak gorde nahi duzu?",
|
||||
"change_password": "Pasahitza aldatu",
|
||||
"password_updated": "Pasahitza eguneratu da",
|
||||
"password_updated": "Pasahitza eguneratu da.",
|
||||
"danger_section": "Atal arriskutsua",
|
||||
"remove_account": "Ondorengo botoia zapalduz gero zure erabiltzailea ezabatuko da. Argitaratutako ekitaldiak ordea, ez dira ezabatuko",
|
||||
"remove_account": "Ondorengo botoia zapalduz gero zure erabiltzailea ezabatuko da. Argitaratutako ekitaldiak ordea, ez dira ezabatuko.",
|
||||
"remove_account_confirm": "Zure kontua behin betiko ezabatzear zaude"
|
||||
},
|
||||
"error": {
|
||||
"nick_taken": "Dagoeneko ezizen hau hartuta dago",
|
||||
"email_taken": "Dagoeneko posta elektroniko hau hartuta dago"
|
||||
"nick_taken": "Dagoeneko ezizen hau hartuta dago.",
|
||||
"email_taken": "Dagoeneko posta elektroniko hau hartuta dago."
|
||||
},
|
||||
"confirm": {
|
||||
"title": "Erabiltzaile-baieztapena",
|
||||
|
@ -226,12 +250,16 @@
|
|||
"5": "bostgarrena",
|
||||
"-1": "azkena"
|
||||
},
|
||||
"about": "<div><h1><strong><u>Descarga la agenda semanal en pdf lista para imprimir pinchando </u></strong><a href='https://lubakiagenda.net/agenda.pdf' rel='noopener noreferrer nofollow'><strong><u>aquí.</u></strong></a></h1><p></p><p><strong>¿Quiénes somos?</strong></p><p>Somos un grupo de personas que gestionamos la lubakiagenda digital y creamos y colgamos la agenda semanal en pdf y papel. No generamos contenido, solamente moderamos el contenido de la web y subimos las actividades de las que nos enteramos.</p><p><strong>¿Qué es LubakiAgenda?</strong></p><p>Es la agenda social alternativa de Bilboalde*. Tiene su versión digital, que cada colectivo o espacio autogestionado puede actualizar con su propia programación, y una versión imprimible que se cuelga cada miércoles en la web y en los lugares más frecuentados de Bilbo.</p><p><strong>¿Cuál es el objetivo de LubakiAgenda?</strong></p><p>Una parte de las actividades que se incluyen en la agenda son las organizadas por gaztetxes, ateneos, distribuidoras,… y por el amplio movimiento popular y juvenil de Bilbo y alrededores. Queremos que esta agenda sea el reflejo de lo que organiza este movimiento en su trabajo cotidiano, resaltando que no hacen falta ni instituciones ni subvenciones para mantener en marcha la cultura popular.</p><p>Nuestro objetivo es dar difusión al movimiento popular desde una perspectiva anticapitalista, antifascista, antirracista, feminista e inclusiva.</p><p>Por ello no se publicarán actividades que vayan en contra de nuestros principios ni por regla general, tampoco actividades comerciales o de agentes sociales que consideremos que ya tienen sus propios medios y fuerzas de difusión y organización (como instituciones, partidos políticos o sindicatos mayoritarios)</p><p><strong>¿Cómo puedo participar?</strong></p><p>Si formas parte de un colectivo social, puedes colgar directamente las actividades que realicéis en el apartado superior derecho de la web (+ Nuevo evento) y solicitar que generemos una usuaria para tu colectivo. De este modo vuestra programación quedará colgada automáticamente sin necesidad de moderación.</p><p>También nos puedes escribir a <a href='mailto:agenda@lubakiagenda.net' rel='noopener noreferrer nofollow'><u>agenda@lubakiagenda.net</u></a> y mandarnos la programación de tu colectivo o espacio y nosotras la subiremos.</p><p>Es muy importante que si quieres que tu programación aparezca en la versión imprimible, <strong>nos hagas llegar la información antes del miércoles al mediodía de cada semana</strong> (si son actividades periódicas, no hace falta que nos mandes mail todas las semanas)</p><p>Si no perteneces a ningún colectivo pero quieres colgar actividades, siempre puedes subirlas mediante el apartado superior derecho de la web (+ Nuevo evento), pero debes saber que el contenido subido será sujeto a moderación para evitar duplicados o actividades contrarias a nuestros principios.</p><h5>¿Zer da 'Gancio'?</h5><p>Gancio, <a href='https://autistici.org/underscore'>Underscore hacklabeko</a> proiektua da eta <a href='https://cisti.org'>cisti.org-eko</a> zerbitzuetariko bat.</p> <p>Gancio ( \"gantzio\" ahoskatzen da) ekitaldiak zabaltzeko tresna da eta komunitate erradikalei zuzenduta dago. Bertan ekitaldiak aurkitu eta sortu daitezke. Gainera, <a href='https://cisti.org'>Cisti.org</a> osoak bezala, Ganciok izaera antisexista, antiarrazista, antifaxista eta antikapitalista dauka, beraz, izan hori buruan ekitaldia argitaratzera zoazenean.</p><h5>Ados, baina ¿zer arraio esan nahi du 'gancio' hitzak?</h5><p>Literalki \"kakoa\" litzateke, baina egia esan Turinen (Italia) erabiltzen den esaeratik dator, hau da, norbaitek esaten badu: \n \"- ehi, ci diamo un gancio alle 8?\" (\"aizu, ¿8etan kakoa emango?\") -zera esan nahi du: \n \"-aizu, ¿8retan elkartuko gara?\". \n\"Darsi un gancio\" hitzordu bat lotzea da, X orduan eta Y lekuan.</p><p><ul><li>¿Zein ordutan da <i>gancio</i>-a manira joateko?</li><li>Ez dakit ta, dena den, ezin naiz joan <i>gancio</i>-a baitaukat bilera baterako.</li></ul></p>\n<h5>Kontaktuak</h5><p>Gancio-ko interfaze berria garatu duzula? Gancio-a abiatu nahi duzula zure hirian? Hobetzeko zerbait bururatu zaizu? Ba, jakin iturri-kodea askea dela eta <a href='https://git.lattuga.net/cisti/gancio'>hemen</a> dagoela eskuragarri. \nLaguntza eta iradokizunak beti direnez ongietorriak, gurekin kontaktuan jarri zaitezkete underscore@autistici.org-en. Ondo izan! </p><p>*Bilboaldea Bilbo, Ezkerraldea, Meatzaldea, Hego Uribe, Uribe Kosta eta Txorierri</p>",
|
||||
"about": "\n <p><a href='https://gancio.org'>Gancio</a> Tokiko komunitateentzako agenda partekatua da.</p>\n ",
|
||||
"oauth": {
|
||||
"authorization_request": "<code>{app}</code> aplikazioak baimena eskatu du <code>{instance_name}</code>-n ondorengo lanak egiteko:",
|
||||
"redirected_to": "Baieztapenaren ondoren <code>{url}</code> helbidera berbideratua izango zara.",
|
||||
"redirected_to": "Baieztapenaren ondoren <code>{url}</code> helbidera berbideratua izango zara",
|
||||
"scopes": {
|
||||
"event:write": "Zure ekitaldiak sortu eta aldatu"
|
||||
}
|
||||
},
|
||||
"validators": {
|
||||
"email": "Sar ezazu posta elektroniko baliozko bat",
|
||||
"required": "{fieldName} beharrezkoa da"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,7 +84,9 @@
|
|||
"address": "Adresse",
|
||||
"where": "Où",
|
||||
"send": "Envoyer",
|
||||
"export": "Exporter"
|
||||
"export": "Exporter",
|
||||
"label": "Nom",
|
||||
"max_events": "Nb. max d'événements"
|
||||
},
|
||||
"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)",
|
||||
|
@ -129,7 +131,9 @@
|
|||
"recurrent_2m_days": "|Le {days} un mois sur deux|Les {jours} un mois sur deux",
|
||||
"recurrent_2w_days": "Un {days} sur deux",
|
||||
"edit_recurrent": "Modifier l’évènement récurrent :",
|
||||
"updated": "Évènement mis à jour"
|
||||
"updated": "Évènement mis à jour",
|
||||
"import_description": "Vous pouvez importer des événements depuis d'autres plateformes ou d'autres instances à travers des formats standards (ics et h-event)",
|
||||
"saved": "Événement enregistré"
|
||||
},
|
||||
"register": {
|
||||
"description": "Les mouvements sociaux doivent s'organiser et s'autofinancer.<br/>\n<br/>Avant de pouvoir publier, <strong> le compte doit être approuvé</strong>, considérez que <strong> derrière ce site vous trouverez de vraies personnes, à qui vous pouvez écrire en deux lignes pour exprimer les évènements que vous souhaiteriez publier.",
|
||||
|
@ -169,7 +173,7 @@
|
|||
"announcement_description": "Dans cette section vous pouvez insérer des annonces qui resteront affichées sur la page d'accueil",
|
||||
"announcement_remove_ok": "Annonce supprimée",
|
||||
"delete_announcement_confirm": "Êtes-vous sûr·e de vouloir supprimer l'annonce ?",
|
||||
"user_block_confirm": "Êtes-vous sûr·e de vouloir bloquer cet utilisateur ?",
|
||||
"user_block_confirm": "Êtes-vous sûr·e de vouloir bloquer l'utilisateur {user} ?",
|
||||
"favicon": "Logo",
|
||||
"user_blocked": "Utilisateur {user} bloqué",
|
||||
"resources": "Ressources",
|
||||
|
@ -196,11 +200,20 @@
|
|||
"allow_registration_description": "Autoriser l'ouverture des inscriptions ?",
|
||||
"user_create_ok": "Utilisateur créé",
|
||||
"user_remove_ok": "Utilisateur supprimé",
|
||||
"delete_user_confirm": "Êtes-vous sûr·e de vouloir supprimer cet administrateur ?",
|
||||
"delete_user_confirm": "Êtes-vous sûr·e de vouloir supprimer {user} ?",
|
||||
"remove_admin": "Supprimer l'administrateur",
|
||||
"delete_user": "Supprimer",
|
||||
"event_confirm_description": "Vous pouvez confirmer les évènements ajoutés par des utilisateurs anonymes ici",
|
||||
"place_description": "Si vous avez donné le mauvais lieu ou la mauvaise adresse, vous pouvez les modifier.<br/>Tous les évènements courants et passés associés à ce lieu seront mis à jour."
|
||||
"place_description": "Si vous avez donné le mauvais lieu ou la mauvaise adresse, vous pouvez les modifier.<br/>Tous les évènements courants et passés associés à ce lieu seront mis à jour.",
|
||||
"add_instance": "Ajouter une instance",
|
||||
"instance_block_confirm": "Êtes-vous sûr·e de vouloir bloquer l'instance {instance} ?",
|
||||
"disable_user_confirm": "Êtes-vous sûr·e de vouloir désactiver {user} ?",
|
||||
"smtp_hostname": "Nom d'hôte SMTP",
|
||||
"smtp_test_button": "Envoyer un e-mail de test",
|
||||
"smtp_description": "<ul><li>L'administrateur reçoit un e-mail lorsqu'un événement anonyme est ajouté (si activé).</li><li>L'administrateur reçoit un e-mail pour chaque demande d'inscription (si activé).</li><li>L'utilisateur reçoit un e-mail suite à sa demande d'inscription.</li><li>L'utilisateur reçoit un e-mail lorsque son inscription est confirmée.</li><li>L'utilisateur reçoit un e-mail de confirmation s'il est inscrit directement par l'administrateur.</li><li>Les utilisateurs reçoivent un e-mail pour restaurer leur mot de passe s'ils l'oublient.</li></ul>",
|
||||
"show_smtp_setup": "Paramètres d'e-mail",
|
||||
"smtp_test_success": "Un e-mail de test a été envoyé à {admin_email}, veuillez vérifier votre boîte de réception",
|
||||
"admin_email": "E-mail de l'administrateur"
|
||||
},
|
||||
"oauth": {
|
||||
"scopes": {
|
||||
|
@ -254,5 +267,11 @@
|
|||
"not_registered": "Pas encore inscrit·e ?",
|
||||
"check_email": "Vérifiez votre boîte de réception et les indésirables.",
|
||||
"description": "En vous connectant vous pouvez publier de nouveaux évènements."
|
||||
},
|
||||
"setup": {
|
||||
"check_db": "Vérifier la base de données",
|
||||
"completed": "Configuration terminée",
|
||||
"completed_description": "<p>Vous pouvez désormais vous connectez avec le compte utilisateur suivant :<br/><br/>Identifiant : <b>{email}</b><br/>Mot de passe : <b>{password}<b/></p>",
|
||||
"start": "Commencer"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -129,7 +129,7 @@
|
|||
"where_description": "Dov'è il gancio? Se il posto non è presente potrai crearlo.",
|
||||
"confirmed": "Evento confermato",
|
||||
"not_found": "Evento non trovato",
|
||||
"remove_confirmation": "Sei sicuro/a di voler eliminare questo evento?",
|
||||
"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:",
|
||||
|
@ -159,7 +159,10 @@
|
|||
"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)"
|
||||
"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 utenti con disabilità visive",
|
||||
"choose_focal_point": "Scegli il punto centrale cliccando",
|
||||
"remove_media_confirmation": "Confermi l'eliminazione dell'immagine?"
|
||||
},
|
||||
"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).",
|
||||
|
@ -191,15 +194,16 @@
|
|||
"hide_resource": "Nascondi risorsa",
|
||||
"show_resource": "Mostra risorsa",
|
||||
"delete_resource": "Elimina risorsa",
|
||||
"delete_resource_confirm": "Sei sicuro/a di voler eliminare questa 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": "Sei sicuro/a di voler bloccare l'utente?",
|
||||
"delete_announcement_confirm": "Sei sicuro/a di voler eliminare l'annuncio?",
|
||||
"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",
|
||||
|
@ -216,9 +220,10 @@
|
|||
"is_dark": "Tema scuro",
|
||||
"add_link": "Aggiungi link",
|
||||
"footer_links": "Collegamenti del piè di pagina",
|
||||
"delete_footer_link_confirm": "Sei sicuro/a di eliminare questo collegamento?",
|
||||
"delete_footer_link_confirm": "Vuoi eliminare questo collegamento?",
|
||||
"edit_place": "Modifica luogo",
|
||||
"new_announcement": "Nuovo annuncio"
|
||||
"new_announcement": "Nuovo annuncio",
|
||||
"show_smtp_setup": "Impostazioni email"
|
||||
},
|
||||
"auth": {
|
||||
"not_confirmed": "Non ancora confermato…",
|
||||
|
@ -260,5 +265,10 @@
|
|||
"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>",
|
||||
"start": "Inizia"
|
||||
}
|
||||
}
|
||||
|
|
11
middleware/setup.js
Normal file
11
middleware/setup.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
export default function ({ req, redirect, route }) {
|
||||
if (process.server) {
|
||||
if (req.firstrun && route.path !== '/setup') {
|
||||
return redirect('/setup')
|
||||
}
|
||||
if (!req.firstrun && route.path === '/setup') {
|
||||
return redirect('/')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
const conf = require('config')
|
||||
const config = require('./server/config.js')
|
||||
|
||||
module.exports = {
|
||||
telemetry: false,
|
||||
|
@ -11,22 +11,30 @@ module.exports = {
|
|||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
|
||||
],
|
||||
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
|
||||
script: [{ src: '/gancio-events.es.js' }],
|
||||
link: [{ rel: 'icon', type: 'image/png', href: '/logo.png' }]
|
||||
},
|
||||
dev: (process.env.NODE_ENV !== 'production'),
|
||||
server: config.server,
|
||||
|
||||
server: conf.server,
|
||||
|
||||
vue: {
|
||||
config: {
|
||||
ignoredElements: ['gancio-events']
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
** Customize the progress-bar color
|
||||
** Customize the progress-bar component
|
||||
*/
|
||||
loading: '~/components/Loading.vue',
|
||||
/*
|
||||
** Global CSS
|
||||
*/
|
||||
css: [
|
||||
'@/assets/style.less',
|
||||
'@mdi/font/css/materialdesignicons.css'
|
||||
'vuetify/dist/vuetify.min.css',
|
||||
'@mdi/font/css/materialdesignicons.css',
|
||||
'@/assets/style.less'
|
||||
],
|
||||
|
||||
/*
|
||||
|
@ -35,31 +43,25 @@ module.exports = {
|
|||
plugins: [
|
||||
'@/plugins/i18n.js',
|
||||
'@/plugins/filters', // text filters, datetime filters, generic transformation helpers etc.
|
||||
'@/plugins/vue-clipboard', // vuetify
|
||||
'@/plugins/vuetify', // vuetify
|
||||
'@/plugins/axios', // axios baseurl configuration
|
||||
'@/plugins/validators', // inject validators
|
||||
'@/plugins/api', // api helpers
|
||||
{ src: '@/plugins/v-calendar', ssr: false } // v-calendar
|
||||
],
|
||||
|
||||
render: {
|
||||
compressor: false,
|
||||
bundleRenderer: {
|
||||
shouldPreload: (file, type) => {
|
||||
return ['script', 'style', 'font'].includes(type)
|
||||
}
|
||||
}
|
||||
},
|
||||
/*
|
||||
** Nuxt.js modules
|
||||
*/
|
||||
modules: [
|
||||
// Doc: https://axios.nuxtjs.org/usage
|
||||
'./@nuxtjs/axios',
|
||||
'./@nuxtjs/auth',
|
||||
['nuxt-express-module', { expressPath: 'server/', routesPath: 'server/routes' }]
|
||||
'@nuxtjs/axios',
|
||||
'@nuxtjs/auth',
|
||||
'@/server/initialize.server.js'
|
||||
],
|
||||
|
||||
serverMiddleware: ['server/routes'],
|
||||
|
||||
/*
|
||||
** Axios module configuration
|
||||
* See https://github.com/nuxt-community/axios-module#options
|
||||
|
@ -93,28 +95,9 @@ module.exports = {
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
buildModules: [
|
||||
'@nuxtjs/vuetify'
|
||||
],
|
||||
vuetify: {
|
||||
defaultAssets: false,
|
||||
optionsPath: './vuetify.options.js',
|
||||
treeShake: true
|
||||
/* module options */
|
||||
},
|
||||
|
||||
/*
|
||||
** Build configuration
|
||||
*/
|
||||
build: {
|
||||
presets: ['@nuxt/babel-preset-app', {
|
||||
useBuiltIns: 'usage', // or "entry"
|
||||
corejs: 3
|
||||
}],
|
||||
babel: {
|
||||
plugins: [['@babel/plugin-proposal-private-methods', { loose: true }]]
|
||||
},
|
||||
cache: true
|
||||
}
|
||||
corejs: 3,
|
||||
cache: true,
|
||||
hardSource: true
|
||||
},
|
||||
}
|
||||
|
|
115
package.json
115
package.json
|
@ -1,19 +1,17 @@
|
|||
{
|
||||
"name": "gancio",
|
||||
"version": "1.0.0-alpha",
|
||||
"version": "1.2.2",
|
||||
"description": "A shared agenda for local communities",
|
||||
"author": "lesion",
|
||||
"scripts": {
|
||||
"build": "cross-env nuxt build --modern",
|
||||
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
|
||||
"dev": "NODE_ENV=development node server/index.js",
|
||||
"dev:nuxt": "cross-env NODE_ENV=development nuxt dev --modern",
|
||||
"build": "nuxt build --modern",
|
||||
"start:inspect": "NODE_ENV=production node --inspect node_modules/.bin/nuxt start --modern",
|
||||
"dev": "nuxt dev",
|
||||
"start": "nuxt start --modern",
|
||||
"doc": "cd docs && bundle exec jekyll b",
|
||||
"doc:dev": "cd docs && bundle exec jekyll s --drafts",
|
||||
"migrate": "NODE_ENV=production sequelize db:migrate",
|
||||
"migrate:dev": "sequelize db:migrate",
|
||||
"start:debug": "cross-env DEBUG=* NODE_ENV=production node server/cli.js",
|
||||
"start": "NODE_ENV=production node server/cli.js"
|
||||
"migrate:dev": "sequelize db:migrate"
|
||||
},
|
||||
"files": [
|
||||
"server/",
|
||||
|
@ -24,113 +22,76 @@
|
|||
"locales/email/",
|
||||
"locales/",
|
||||
"store/",
|
||||
"config/default.json",
|
||||
"config/production.js",
|
||||
".nuxt/",
|
||||
"yarn.lock"
|
||||
],
|
||||
"dependencies": {
|
||||
"@nuxtjs/auth": "^4.9.1",
|
||||
"@nuxtjs/axios": "^5.13.5",
|
||||
"@popperjs/core": "2.9.2",
|
||||
"accept-language": "^3.0.18",
|
||||
"axios": "^0.21.1",
|
||||
"axios": "^0.24.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.18.3",
|
||||
"bufferutil": "^4.0.1",
|
||||
"config": "^3.3.6",
|
||||
"consola": "^2.15.3",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"core-js": "3.14.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"date-fns": "^2.21.3",
|
||||
"dayjs": "^1.10.5",
|
||||
"dompurify": "^2.2.9",
|
||||
"email-templates": "^8.0.7",
|
||||
"dayjs": "^1.10.7",
|
||||
"dompurify": "^2.3.3",
|
||||
"email-templates": "^8.0.8",
|
||||
"express": "^4.17.1",
|
||||
"express-oauth-server": "^2.0.0",
|
||||
"express-prom-bundle": "^6.3.4",
|
||||
"fs": "^0.0.1-security",
|
||||
"global": "^4.4.0",
|
||||
"http-signature": "^1.3.5",
|
||||
"express-oauth-server": "lesion/express-oauth-server#master",
|
||||
"http-signature": "^1.3.6",
|
||||
"ical.js": "^1.4.0",
|
||||
"ics": "^2.27.0",
|
||||
"inquirer": "^8.1.1",
|
||||
"jsdom": "^16.6.0",
|
||||
"ics": "^2.35.0",
|
||||
"jsdom": "^18.1.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"less": "^4.1.1",
|
||||
"linkifyjs": "3.0.0-beta.3",
|
||||
"linkify-html": "^3.0.4",
|
||||
"linkifyjs": "3.0.4",
|
||||
"lodash": "^4.17.21",
|
||||
"microformat-node": "^2.0.1",
|
||||
"mkdirp": "^1.0.4",
|
||||
"multer": "^1.4.2",
|
||||
"nuxt": "^2.15.7",
|
||||
"nuxt-express-module": "^0.0.11",
|
||||
"multer": "^1.4.3",
|
||||
"nuxt-edge": "^2.16.0-27305297.ab1c6cb4",
|
||||
"pg": "^8.6.0",
|
||||
"pg-native": "3.0.0",
|
||||
"prom-client": "^13.1.0",
|
||||
"sequelize": "^6.6.2",
|
||||
"sequelize-cli": "^6.2.0",
|
||||
"sequelize-slugify": "^1.5.0",
|
||||
"sharp": "^0.28.2",
|
||||
"sqlite3": "^5.0.2",
|
||||
"sequelize": "^6.12.0-alpha.1",
|
||||
"sequelize-slugify": "^1.6.0",
|
||||
"sharp": "^0.27.2",
|
||||
"sqlite3": "mapbox/node-sqlite3#918052b",
|
||||
"tiptap": "^1.32.0",
|
||||
"tiptap-extensions": "^1.35.0",
|
||||
"to-ico": "^1.1.5",
|
||||
"url": "^0.11.0",
|
||||
"utf-8-validate": "^5.0.5",
|
||||
"v-calendar": "2.3.0",
|
||||
"umzug": "^2.3.0",
|
||||
"v-calendar": "2.3.4",
|
||||
"vue": "^2.6.14",
|
||||
"vue-clipboard2": "^0.3.1",
|
||||
"vue-i18n": "^8.24.4",
|
||||
"vue-server-renderer": "^2.6.14",
|
||||
"vue-i18n": "^8.26.7",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"vuetify": "^2.6.1",
|
||||
"winston": "^3.3.3",
|
||||
"winston-daily-rotate-file": "^4.5.5",
|
||||
"yargs": "^17.0.1"
|
||||
"yargs": "^17.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mdi/font": "^5.9.55",
|
||||
"@nuxtjs/eslint-config": "^6.0.1",
|
||||
"@nuxtjs/vuetify": "^1.12.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
||||
"@typescript-eslint/parser": "^4.26.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^7.27.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-loader": "^4.0.2",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"eslint-plugin-node": ">=11.1.0",
|
||||
"eslint-plugin-nuxt": "^2.0.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"eslint-plugin-vue": "^7.10.0",
|
||||
"fibers": "^5.0.0",
|
||||
"less-loader": "7",
|
||||
"@mdi/font": "^6.5.95",
|
||||
"less": "^4.1.1",
|
||||
"less-loader": "^7",
|
||||
"prettier": "^2.3.0",
|
||||
"pug": "^3.0.2",
|
||||
"pug-plain-loader": "^1.1.0",
|
||||
"sass": "^1.32.12",
|
||||
"sass-loader": "10",
|
||||
"typescript": "^4.3.4",
|
||||
"vue-cli-plugin-vuetify": "~2.4.0",
|
||||
"vuetify": "^2.5.4",
|
||||
"vuetify-loader": "^1.7.1",
|
||||
"sass": "^1.43.5",
|
||||
"sequelize-cli": "^6.3.0",
|
||||
"webpack": "4",
|
||||
"webpack-cli": "^4.7.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"prosemirror-model": "1.14.1",
|
||||
"source-map-resolve": "0.6.0",
|
||||
"lodash": "4.17.21",
|
||||
"minimist": "1.2.5",
|
||||
"jimp": "0.16.1",
|
||||
"resize-img": "2.0.0",
|
||||
"underscore": "1.13.1",
|
||||
"@nuxtjs/vuetify/**/sass": "1.32.12"
|
||||
"@nuxtjs/vuetify/**/sass": "1.32.12",
|
||||
"postcss": "7.0.36",
|
||||
"glob-parent": "5.1.2",
|
||||
"chokidar": "3.5.2",
|
||||
"core-js": "3.19.0"
|
||||
},
|
||||
"bin": {
|
||||
"gancio": "server/cli.js"
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
<template lang='pug'>
|
||||
v-row.mt-5(align='center' justify='center')
|
||||
v-col(cols='12' md="6" lg="5" xl="4")
|
||||
v-card(light)
|
||||
v-card-title {{settings.title}} - {{$t('common.authorize')}}
|
||||
v-card-text
|
||||
u {{$auth.user.email}}
|
||||
div
|
||||
p(v-html="$t('oauth.authorization_request', { app: client.name, instance_name: settings.title })")
|
||||
ul
|
||||
li(v-for="s in scope.split(' ')") {{$t(`oauth.scopes.${scope}`)}}
|
||||
span(v-html="$t('oauth.redirected_to', {url: $route.query.redirect_uri})")
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(color='error' to='/') {{$t('common.cancel')}}
|
||||
v-btn(:href='authorizeURL' color='success') {{$t('common.authorize')}}
|
||||
.d-flex.justify-space-around
|
||||
v-card.mt-5(max-width='600px')
|
||||
v-card-title {{settings.title}} - {{$t('common.authorize')}}
|
||||
v-card-text
|
||||
u {{$auth.user.email}}
|
||||
div
|
||||
p(v-html="$t('oauth.authorization_request', { app: client.name, 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})")
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(color='error' to='/') {{$t('common.cancel')}}
|
||||
v-btn(:href='authorizeURL' color='success') {{$t('common.authorize')}}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -71,7 +70,7 @@ export default {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
<style lang='less'>
|
||||
<style>
|
||||
h4 img {
|
||||
max-height: 40px;
|
||||
border-radius: 20px;
|
||||
|
|
|
@ -11,13 +11,13 @@
|
|||
v-text-field(v-model='email' type='email'
|
||||
validate-on-blur
|
||||
:rules='$validators.email' autofocus
|
||||
:placeholder='$t("common.email")'
|
||||
:label='$t("common.email")'
|
||||
ref='email')
|
||||
|
||||
v-text-field(v-model='password'
|
||||
:rules='$validators.password'
|
||||
type='password'
|
||||
:placeholder='$t("common.password")')
|
||||
:label='$t("common.password")')
|
||||
|
||||
v-card-actions
|
||||
v-btn(text
|
||||
|
|
|
@ -7,8 +7,10 @@ v-col(cols=12)
|
|||
v-btn(v-if='settings.allow_recurrent_event' value='recurrent' label="recurrent") {{$t('event.recurrent')}}
|
||||
|
||||
p {{$t(`event.${type}_description`)}}
|
||||
|
||||
v-btn-toggle.v-col-6.flex-column.flex-sm-row(v-if='type === "recurrent"' color='primary' :value='value.recurrent.frequency' @change='fq => change("frequency", fq)')
|
||||
v-btn(v-for='f in frequencies' :key='f.value' :value='f.value') {{f.text}}
|
||||
|
||||
client-only
|
||||
.datePicker.mt-3
|
||||
v-input(:value='fromDate'
|
||||
|
@ -43,11 +45,6 @@ v-col(cols=12)
|
|||
:value='dueHour' clearable
|
||||
:items='hourList' @change='hr => change("dueHour", hr)')
|
||||
|
||||
//- div.col-md-12(v-if='isRecurrent')
|
||||
//- p(v-if='value.recurrent.frequency !== "1m" && value.recurrent.frequency !== "2m"') 🡲 {{whenPatterns}}
|
||||
//- v-btn-toggle(v-else dense group v-model='value.recurrent.type' color='primary')
|
||||
//- v-btn(text link v-for='whenPattern in whenPatterns' :value='whenPattern.key' :key='whenPatterns.key') {{whenPattern.label}}
|
||||
|
||||
List(v-if='type==="normal" && todayEvents.length' :events='todayEvents' :title='$t("event.same_day")')
|
||||
|
||||
</template>
|
||||
|
@ -61,17 +58,13 @@ 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 }) },
|
||||
event: { type: Object, default: () => null }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
type: 'normal',
|
||||
time: { start: null, end: null },
|
||||
fromDateMenu: null,
|
||||
dueDateMenu: null,
|
||||
date: null,
|
||||
page: null,
|
||||
frequency: '',
|
||||
events: [],
|
||||
frequencies: [
|
||||
{ value: '1w', text: this.$t('event.each_week') },
|
||||
|
@ -85,7 +78,7 @@ export default {
|
|||
todayEvents () {
|
||||
const start = dayjs(this.value.from).startOf('day').unix()
|
||||
const end = dayjs(this.value.from).endOf('day').unix()
|
||||
const events = this.events.filter(e => e.start_datetime >= start && e.start_datetime <= end)
|
||||
const events = this.events.filter(e => (this.event.id && e.id !== this.event.id) && e.start_datetime >= start && e.start_datetime <= end)
|
||||
return events
|
||||
},
|
||||
attributes () {
|
||||
|
@ -106,16 +99,15 @@ export default {
|
|||
},
|
||||
hourList () {
|
||||
const hourList = []
|
||||
const pad = '00'
|
||||
const leftPad = h => ('00' + h).slice(-2)
|
||||
for (let h = 0; h < 24; h++) {
|
||||
hourList.push(`${(pad + h).slice(-pad.length)}:00`)
|
||||
hourList.push(`${(pad + h).slice(-pad.length)}:30`)
|
||||
const textHour = leftPad(h < 13 ? h : h - 12)
|
||||
hourList.push({ text: textHour + ':00 ' + (h <= 12 ? 'AM' : 'PM'), value: leftPad(h) + ':00' })
|
||||
hourList.push({ text: textHour + ':30 ' + (h <= 12 ? 'AM' : 'PM'), value: leftPad(h) + ':30' })
|
||||
}
|
||||
|
||||
return hourList
|
||||
},
|
||||
isRecurrent () {
|
||||
return !!this.value.recurrent
|
||||
},
|
||||
whenPatterns () {
|
||||
if (!this.value.from) { return }
|
||||
const date = dayjs(this.value.from)
|
||||
|
@ -181,7 +173,7 @@ export default {
|
|||
if (what === 'type') {
|
||||
if (typeof value === 'undefined') { this.type = 'normal' }
|
||||
if (value === 'recurrent') {
|
||||
this.$emit('input', { ...this.value, recurrent: {}, multidate: false })
|
||||
this.$emit('input', { ...this.value, recurrent: { frequency: '1w' }, multidate: false })
|
||||
} else if (value === 'multidate') {
|
||||
this.$emit('input', { ...this.value, recurrent: null, multidate: true })
|
||||
} else {
|
||||
|
@ -213,14 +205,14 @@ export default {
|
|||
const fromHour = dayjs(this.value.from).hour()
|
||||
|
||||
// add a day
|
||||
let due = dayjs(this.value.due)
|
||||
let due = dayjs(this.value.from)
|
||||
if (fromHour > Number(hour) && !this.value.multidate) {
|
||||
due = due.add(1, 'day')
|
||||
}
|
||||
due = due.hour(hour).minute(minute)
|
||||
this.$emit('input', { ...this.value, due, dueHour: true })
|
||||
} else {
|
||||
this.$emit('input', { ...this.value, dueHour: false })
|
||||
this.$emit('input', { ...this.value, due: null, dueHour: false })
|
||||
}
|
||||
// change date in calendar (could be a range or a recurrent event...)
|
||||
} else if (what === 'date') {
|
||||
|
@ -240,30 +232,22 @@ export default {
|
|||
this.$emit('input', { ...this.value, from, due })
|
||||
} else {
|
||||
let from = value
|
||||
let due = value
|
||||
let due = this.value.due
|
||||
if (this.value.fromHour) {
|
||||
from = dayjs(value).hour(dayjs(this.value.from).hour())
|
||||
}
|
||||
if (this.value.dueHour) {
|
||||
if (this.value.dueHour && this.value.due) {
|
||||
due = dayjs(value).hour(dayjs(this.value.due).hour())
|
||||
}
|
||||
this.$emit('input', { ...this.value, from, due })
|
||||
}
|
||||
}
|
||||
},
|
||||
changeType (type) {
|
||||
if (type === 'recurrent') {
|
||||
this.updateRecurrent({})
|
||||
}
|
||||
},
|
||||
selectFrequency (f) {
|
||||
this.$emit('input', { recurrent: { frequency: f }, from: this.value.from, due: this.value.due })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
<style>
|
||||
.datePicker {
|
||||
max-width: 500px !important;
|
||||
margin: 0 auto;
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
p(v-html="$t('event.import_description')")
|
||||
v-form(v-model='valid' ref='form' lazy-validation @submit.prevent='importGeneric')
|
||||
v-row
|
||||
v-col
|
||||
.col-xl-5.col-lg-5.col-md-7.col-sm-12.col-xs-12
|
||||
v-text-field(v-model='URL'
|
||||
:label="$t('common.url')"
|
||||
:hint="$t('event.import_URL')"
|
||||
persistent-hint
|
||||
:loading='loading' :error='error'
|
||||
:error-messages='errorMessage')
|
||||
v-col
|
||||
.col
|
||||
v-file-input(
|
||||
v-model='file'
|
||||
accept=".ics"
|
||||
|
@ -22,8 +22,8 @@
|
|||
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(@click='$emit("close")' color='warning') {{$t('common.cancel')}}
|
||||
v-btn(@click='importGeneric' :loading='loading' :disabled='loading'
|
||||
v-btn(text @click='$emit("close")' color='warning') {{$t('common.cancel')}}
|
||||
v-btn(text @click='importGeneric' :loading='loading' :disabled='loading'
|
||||
color='primary') {{$t('common.import')}}
|
||||
|
||||
</template>
|
||||
|
|
175
pages/add/MediaInput.vue
Normal file
175
pages/add/MediaInput.vue
Normal file
|
@ -0,0 +1,175 @@
|
|||
<template lang="pug">
|
||||
span
|
||||
v-dialog(v-model='openMediaDetails' :fullscreen="$vuetify.breakpoint.xsOnly" width='1000px')
|
||||
v-card
|
||||
v-card-title {{$t('common.media')}}
|
||||
v-card-text
|
||||
v-row.mt-1
|
||||
v-col#focalPointSelector(
|
||||
@mousedown='handleStart' @touchstart='handleStart'
|
||||
@mousemove='handleMove' @touchmove='handleMove'
|
||||
@mouseup='handleStop' @touchend='handleStop'
|
||||
)
|
||||
div.focalPoint(:style="{ top, left }")
|
||||
img(v-if='mediaPreview' :src='mediaPreview')
|
||||
|
||||
v-col.col-12.col-sm-4
|
||||
p {{$t('event.choose_focal_point')}}
|
||||
img.img.d-none.d-sm-block(v-if='mediaPreview'
|
||||
:src='mediaPreview' :style="{ 'object-position': position }")
|
||||
|
||||
v-textarea.mt-4(type='text'
|
||||
label='Alternative text'
|
||||
persistent-hint
|
||||
@input='v => name=v'
|
||||
:value='value.name' filled
|
||||
:hint='$t("event.alt_text_description")')
|
||||
br
|
||||
v-card-actions.justify-space-between
|
||||
v-btn(text @click='openMediaDetails=false' color='warning') Cancel
|
||||
v-btn(text color='primary' @click='save') Save
|
||||
|
||||
h3.mb-3.font-weight-regular(v-if='mediaPreview') {{$t('common.media')}}
|
||||
v-card-actions(v-if='mediaPreview')
|
||||
v-spacer
|
||||
v-btn(text color='primary' @click='openMediaDetails = true') {{$t('common.edit')}}
|
||||
v-btn(text color='error' @click='remove') {{$t('common.remove')}}
|
||||
div(v-if='mediaPreview')
|
||||
img.img.col-12.ml-3(:src='mediaPreview' :style="{ 'object-position': savedPosition }")
|
||||
span.float-right {{event.media[0].name}}
|
||||
v-file-input(
|
||||
v-else
|
||||
:label="$t('common.media')"
|
||||
:hint="$t('event.media_description')"
|
||||
prepend-icon="mdi-camera"
|
||||
:value='value.image'
|
||||
@change="selectMedia"
|
||||
persistent-hint
|
||||
accept='image/*')
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'MediaInput',
|
||||
props: {
|
||||
value: { type: Object, default: () => ({ image: null }) },
|
||||
event: { type: Object, default: () => {} }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
openMediaDetails: false,
|
||||
name: this.value.name || '',
|
||||
focalpoint: this.value.focalpoint || [0, 0],
|
||||
dragging: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
mediaPreview () {
|
||||
if (!this.value.url && !this.value.image) {
|
||||
return false
|
||||
}
|
||||
const url = this.value.image ? URL.createObjectURL(this.value.image) : /^https?:\/\//.test(this.value.url) ? this.value.url : `/media/thumb/${this.value.url}`
|
||||
return url
|
||||
},
|
||||
top () {
|
||||
return ((this.focalpoint[1] + 1) * 50) + '%'
|
||||
},
|
||||
left () {
|
||||
return ((this.focalpoint[0] + 1) * 50) + '%'
|
||||
},
|
||||
savedPosition () {
|
||||
const focalpoint = this.value.focalpoint || [0, 0]
|
||||
return `${(focalpoint[0] + 1) * 50}% ${(focalpoint[1] + 1) * 50}%`
|
||||
},
|
||||
position () {
|
||||
const focalpoint = this.focalpoint || [0, 0]
|
||||
return `${(focalpoint[0] + 1) * 50}% ${(focalpoint[1] + 1) * 50}%`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save () {
|
||||
this.$emit('input', { url: this.value.url, image: this.value.image, name: this.name || this.value.image.name || '', focalpoint: [...this.focalpoint] })
|
||||
this.openMediaDetails = false
|
||||
},
|
||||
async remove () {
|
||||
const ret = await this.$root.$confirm('event.remove_media_confirmation')
|
||||
if (!ret) { return }
|
||||
this.$emit('remove')
|
||||
},
|
||||
selectMedia (v) {
|
||||
this.$emit('input', { image: v, name: v.name, focalpoint: [0, 0] })
|
||||
},
|
||||
handleStart (ev) {
|
||||
ev.preventDefault()
|
||||
this.dragging = true
|
||||
this.handleMove(ev, true)
|
||||
return false
|
||||
},
|
||||
handleStop (ev) {
|
||||
this.dragging = false
|
||||
},
|
||||
handleMove (ev, manual = false) {
|
||||
if (!this.dragging && !manual) return
|
||||
ev.stopPropagation()
|
||||
const boundingClientRect = document.getElementById('focalPointSelector').getBoundingClientRect()
|
||||
|
||||
const clientX = ev.changedTouches ? ev.changedTouches[0].clientX : ev.clientX
|
||||
const clientY = ev.changedTouches ? ev.changedTouches[0].clientY : ev.clientY
|
||||
|
||||
// get relative coordinate
|
||||
let x = Math.ceil(clientX - boundingClientRect.left)
|
||||
let y = Math.ceil(clientY - boundingClientRect.top)
|
||||
|
||||
// snap to border
|
||||
x = x < 30 ? 0 : x > boundingClientRect.width - 30 ? boundingClientRect.width : x
|
||||
y = y < 30 ? 0 : y > boundingClientRect.height - 30 ? boundingClientRect.height : y
|
||||
|
||||
// this.relativeFocalpoint = [x + 'px', y + 'px']
|
||||
|
||||
// map to real image coordinate
|
||||
const posY = -1 + (y / boundingClientRect.height) * 2
|
||||
const posX = -1 + (x / boundingClientRect.width) * 2
|
||||
|
||||
this.focalpoint = [posX, posY]
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.cursorPointer {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.img {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
object-position: top;
|
||||
aspect-ratio: 1.7778;
|
||||
}
|
||||
|
||||
#focalPointSelector {
|
||||
position: relative;
|
||||
cursor: move;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-self: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#focalPointSelector img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.focalPoint {
|
||||
position: absolute;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
top: 100px;
|
||||
left: 100px;
|
||||
transform: translate(-25px, -25px);
|
||||
border-radius: 50%;
|
||||
border: 1px solid #ff6d408e;
|
||||
box-shadow: 0 0 0 9999em rgba(0, 0, 0, .65);
|
||||
}
|
||||
</style>
|
|
@ -6,7 +6,7 @@
|
|||
v-spacer
|
||||
v-btn(link text color='primary' @click='openImportDialog=true')
|
||||
<v-icon>mdi-file-import</v-icon> {{$t('common.import')}}
|
||||
v-dialog(v-model='openImportDialog')
|
||||
v-dialog(v-model='openImportDialog' :fullscreen='$vuetify.breakpoint.xsOnly')
|
||||
ImportDialog(@close='openImportDialog=false' @imported='eventImported')
|
||||
|
||||
v-card-text.px-0.px-xs-2
|
||||
|
@ -33,8 +33,7 @@
|
|||
WhereInput(ref='where' v-model='event.place')
|
||||
|
||||
//- When
|
||||
DateInput(v-model='date')
|
||||
|
||||
DateInput(v-model='date' :event='event')
|
||||
//- Description
|
||||
v-col.px-0(cols='12')
|
||||
Editor.px-3.ma-0(
|
||||
|
@ -45,14 +44,7 @@
|
|||
|
||||
//- MEDIA / FLYER / POSTER
|
||||
v-col(cols=12 md=6)
|
||||
v-file-input(
|
||||
:label="$t('common.media')"
|
||||
:hint="$t('event.media_description')"
|
||||
prepend-icon="mdi-camera"
|
||||
v-model='event.image'
|
||||
persistent-hint
|
||||
accept='image/*')
|
||||
v-img.col-12.col-sm-2.ml-3(v-if='mediaPreview' :src='mediaPreview')
|
||||
MediaInput(v-model='event.media[0]' :event='event' @remove='event.media=[]')
|
||||
|
||||
//- tags
|
||||
v-col(cols=12 md=6)
|
||||
|
@ -66,7 +58,7 @@
|
|||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(@click='done' :loading='loading' :disabled='!valid || loading'
|
||||
color='primary') {{edit?$t('common.edit'):$t('common.send')}}
|
||||
color='primary') {{edit?$t('common.save'):$t('common.send')}}
|
||||
|
||||
</template>
|
||||
<script>
|
||||
|
@ -77,16 +69,17 @@ import List from '@/components/List'
|
|||
import ImportDialog from './ImportDialog'
|
||||
import DateInput from './DateInput'
|
||||
import WhereInput from './WhereInput'
|
||||
import MediaInput from './MediaInput'
|
||||
|
||||
export default {
|
||||
name: 'NewEvent',
|
||||
components: { List, Editor, ImportDialog, WhereInput, DateInput },
|
||||
components: { List, Editor, ImportDialog, MediaInput, WhereInput, DateInput },
|
||||
validate ({ store }) {
|
||||
return (store.state.auth.loggedIn || store.state.settings.allow_anon_event)
|
||||
},
|
||||
async asyncData ({ params, $axios, error, store }) {
|
||||
if (params.edit) {
|
||||
const data = { event: { place: {} } }
|
||||
const data = { event: { place: {}, media: [] } }
|
||||
data.id = params.edit
|
||||
data.edit = true
|
||||
let event
|
||||
|
@ -112,7 +105,7 @@ export default {
|
|||
data.event.description = event.description
|
||||
data.event.id = event.id
|
||||
data.event.tags = event.tags
|
||||
data.event.image_path = event.image_path
|
||||
data.event.media = event.media || []
|
||||
return data
|
||||
}
|
||||
return {}
|
||||
|
@ -128,15 +121,14 @@ export default {
|
|||
title: '',
|
||||
description: '',
|
||||
tags: [],
|
||||
image: null
|
||||
media: []
|
||||
},
|
||||
page: { month, year },
|
||||
fileList: [],
|
||||
id: null,
|
||||
date: { from: 0, due: 0, recurrent: null },
|
||||
date: { from: null, due: null, recurrent: null },
|
||||
edit: false,
|
||||
loading: false,
|
||||
mediaUrl: '',
|
||||
disableAddress: false
|
||||
}
|
||||
},
|
||||
|
@ -145,16 +137,7 @@ export default {
|
|||
title: `${this.settings.title} - ${this.$t('common.add_event')}`
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['tags', 'places', 'settings']),
|
||||
mediaPreview () {
|
||||
if (!this.event.image && !this.event.image_path) {
|
||||
return false
|
||||
}
|
||||
const url = this.event.image ? URL.createObjectURL(this.event.image) : `/media/thumb/${this.event.image_path}`
|
||||
return url
|
||||
}
|
||||
},
|
||||
computed: mapState(['tags', 'places', 'settings']),
|
||||
methods: {
|
||||
...mapActions(['updateMeta']),
|
||||
eventImported (event) {
|
||||
|
@ -170,9 +153,6 @@ export default {
|
|||
}
|
||||
this.openImportDialog = false
|
||||
},
|
||||
cleanFile () {
|
||||
this.event.image = {}
|
||||
},
|
||||
async done () {
|
||||
if (!this.$refs.form.validate()) {
|
||||
this.$nextTick(() => {
|
||||
|
@ -187,16 +167,20 @@ export default {
|
|||
|
||||
formData.append('recurrent', JSON.stringify(this.date.recurrent))
|
||||
|
||||
if (this.event.image) {
|
||||
formData.append('image', this.event.image)
|
||||
if (this.event.media.length) {
|
||||
formData.append('image', this.event.media[0].image)
|
||||
formData.append('image_url', this.event.media[0].url)
|
||||
formData.append('image_name', this.event.media[0].name)
|
||||
formData.append('image_focalpoint', this.event.media[0].focalpoint)
|
||||
}
|
||||
|
||||
formData.append('title', this.event.title)
|
||||
formData.append('place_name', this.event.place.name)
|
||||
formData.append('place_address', this.event.place.address)
|
||||
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())
|
||||
formData.append('end_datetime', this.date.due ? dayjs(this.date.due).unix() : this.date.from.add(2, 'hour').unix())
|
||||
|
||||
if (this.edit) {
|
||||
formData.append('id', this.event.id)
|
||||
|
@ -219,7 +203,7 @@ export default {
|
|||
this.$root.$message('event.image_too_big', { color: 'error' })
|
||||
break
|
||||
default:
|
||||
this.$root.$message(e.response.data, { color: 'error' })
|
||||
this.$root.$message(e.response ? e.response.data : e, { color: 'error' })
|
||||
}
|
||||
this.loading = false
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template lang="pug">
|
||||
nuxt-link.embed_event(:to='`/event/${event.slug || event.id}`' target='_blank' :class='{ withImg: event.image_path }')
|
||||
nuxt-link.embed_event(:to='`/event/${event.slug || event.id}`' target='_blank' :class='{ withImg: event.media }')
|
||||
|
||||
//- image
|
||||
img.float-left(:src='`/media/thumb/${event.image_path || "logo.png"}`')
|
||||
img.float-left(:src='event | mediaURL("thumb")')
|
||||
.event-info
|
||||
//- title
|
||||
.date {{event|when}}<br/>
|
||||
|
@ -37,7 +37,7 @@ export default {
|
|||
.embed_event {
|
||||
display: flex;
|
||||
transition: margin .1s;
|
||||
background: url('/favicon.ico') no-repeat right 5px bottom 5px;
|
||||
background: url('/logo.png') no-repeat right 5px bottom 5px;
|
||||
background-size: 32px;
|
||||
background-color: #1f1f1f;
|
||||
text-decoration: none;
|
||||
|
|
|
@ -11,10 +11,12 @@ export default {
|
|||
const title = query.title
|
||||
const tags = query.tags
|
||||
const places = query.places
|
||||
const show_recurrent = !!query.show_recurrent
|
||||
|
||||
let params = []
|
||||
if (places) { params.push(`places=${places}`) }
|
||||
if (tags) { params.push(`tags=${tags}`) }
|
||||
if (show_recurrent) { params.push('show_recurrent=1') }
|
||||
|
||||
params = params.length ? `?${params.join('&')}` : ''
|
||||
const events = await $axios.$get(`/export/json${params}`)
|
||||
|
|
|
@ -11,13 +11,14 @@ v-container#event.pa-0.pa-sm-2
|
|||
v-row
|
||||
v-col.col-12.col-lg-8
|
||||
//- fake image to use u-featured in h-event microformat
|
||||
img.u-featured(v-show='false' :src='`${settings.baseurl}${imgPath}`')
|
||||
img.u-featured(v-show='false' v-if='hasMedia' :src='event | mediaURL')
|
||||
v-img.main_image.mb-3(
|
||||
contain
|
||||
:src='imgPath'
|
||||
:lazy-src='thumbImgPath'
|
||||
v-if='event.image_path')
|
||||
.p-description.text-body-1.pa-3.grey.darken-4.rounded(v-else v-html='event.description')
|
||||
:alt='event | mediaURL("alt")'
|
||||
:src='event | mediaURL'
|
||||
:lazy-src='event | mediaURL("thumb")'
|
||||
v-if='hasMedia')
|
||||
.p-description.text-body-1.pa-3.rounded(v-if='!hasMedia && event.description' v-html='event.description')
|
||||
|
||||
v-col.col-12.col-lg-4
|
||||
v-card
|
||||
|
@ -34,21 +35,20 @@ v-container#event.pa-0.pa-sm-2
|
|||
|
||||
.text-h6.p-location
|
||||
v-icon mdi-map-marker
|
||||
b.vcard.ml-2 {{event.place.name}}
|
||||
.text-subtitle-1.adr {{event.place.address}}
|
||||
b.vcard.ml-2 {{event.place && event.place.name}}
|
||||
.text-subtitle-1.adr {{event.place && event.place.address}}
|
||||
|
||||
//- tags, hashtags
|
||||
v-card-text(v-if='event.tags.length')
|
||||
v-chip.p-category.ml-1.mt-3(v-for='tag in event.tags' color='primary'
|
||||
outlined :key='tag' v-text='tag')
|
||||
outlined :key='tag')
|
||||
span(v-text='tag')
|
||||
|
||||
//- info & actions
|
||||
v-toolbar
|
||||
v-tooltip(bottom) {{$t('common.copy_link')}}
|
||||
template(v-slot:activator="{on, attrs} ")
|
||||
v-btn.ml-2(large icon v-on='on' color='primary'
|
||||
v-clipboard:success='copyLink'
|
||||
v-clipboard:copy='`${settings.baseurl}/event/${event.slug || event.id}`')
|
||||
v-btn.ml-2(large icon v-on='on' color='primary' @click='clipboard(`${settings.baseurl}/event/${event.slug || event.id}`)')
|
||||
v-icon mdi-content-copy
|
||||
v-tooltip(bottom) {{$t('common.embed')}}
|
||||
template(v-slot:activator="{on, attrs} ")
|
||||
|
@ -60,35 +60,43 @@ v-container#event.pa-0.pa-sm-2
|
|||
:href='`/api/event/${event.slug || event.id}.ics`')
|
||||
v-icon mdi-calendar-export
|
||||
|
||||
.p-description.text-body-1.pa-3.grey.darken-4.rounded(v-if='event.image_path && event.description' v-html='event.description')
|
||||
.p-description.text-body-1.pa-3.rounded(v-if='hasMedia && event.description' v-html='event.description')
|
||||
|
||||
//- resources from fediverse
|
||||
#resources.mt-1(v-if='settings.enable_federation')
|
||||
div.float-right(v-if='!settings.hide_boosts')
|
||||
small.mr-3 🔖 {{event.likes.length}}
|
||||
small ✊ {{event.boost.length}}<br/>
|
||||
//- div.float-right(v-if='settings.hide_boosts')
|
||||
//- small.mr-3 🔖 {{event.likes.length}}
|
||||
//- small ✊ {{event.boost.length}}<br/>
|
||||
|
||||
v-dialog.showResource#resourceDialog(v-model='showResources' fullscreen
|
||||
width='95vw'
|
||||
v-dialog(v-model='showResources'
|
||||
fullscreen
|
||||
destroy-on-close
|
||||
@keydown.native.right='$refs.carousel.next()'
|
||||
@keydown.native.left='$refs.carousel.prev()')
|
||||
v-carousel(:interval='10000' ref='carousel' arrow='always')
|
||||
v-carousel-item(v-for='attachment in selectedResource.data.attachment' :key='attachment.url')
|
||||
v-img(:src='attachment.url')
|
||||
v-list.mb-1(v-if='settings.enable_resources' v-for='resource in event.resources' dark
|
||||
:key='resource.id' :class='{disabled: resource.hidden}')
|
||||
v-list-item
|
||||
v-list-title
|
||||
scrollable
|
||||
transition='dialog-bottom-transition')
|
||||
v-card
|
||||
v-btn.ma-2(icon dark @click='showResources = false')
|
||||
v-icon mdi-close
|
||||
v-carousel.pa-5(:interval='10000' ref='carousel' hide-delimiters v-model='currentAttachment'
|
||||
height='100%' show-arrows-on-over)
|
||||
v-carousel-item(v-for='attachment in selectedResource.data.attachment'
|
||||
v-if='isImg(attachment)'
|
||||
:key='attachment.url')
|
||||
v-img(:src='attachment.url' contain max-width='100%' max-height='100%')
|
||||
v-card-actions.align-center.justify-center
|
||||
span {{currentAttachmentLabel}}
|
||||
|
||||
v-card.grey.darken-4.mb-3#resources(v-if='settings.enable_resources' v-for='resource in event.resources'
|
||||
:key='resource.id' :class='{disabled: resource.hidden}' elevation='10' outlined)
|
||||
v-card-title
|
||||
v-menu(v-if='$auth.user && $auth.user.is_admin' offset-y)
|
||||
template(v-slot:activator="{ on, attrs }")
|
||||
v-btn.mr-2(v-on='on' v-attrs='attrs' color='primary' small icon outlined)
|
||||
template(v-slot:activator="{ on }")
|
||||
v-btn.mr-2(v-on='on' color='primary' small icon)
|
||||
v-icon mdi-dots-vertical
|
||||
v-list
|
||||
v-list-item(v-if='!resource.hidden' @click='hideResource(resource, true)')
|
||||
v-list-item-title <v-icon left>mdi-eye-off</v-icon> {{$t('admin.hide_resource')}}
|
||||
v-list-item(v-else @click='hideResource(resource, false)')
|
||||
v-list-item-title <v-icon left>mdi-eye-on</v-icon> {{$t('admin.show_resource')}}
|
||||
v-list-item-title <v-icon left>mdi-eye</v-icon> {{$t('admin.show_resource')}}
|
||||
v-list-item(@click='deleteResource(resource)')
|
||||
v-list-item-title <v-icon left>mdi-delete</v-icon> {{$t('admin.delete_resource')}}
|
||||
v-list-item(@click='blockUser(resource)')
|
||||
|
@ -97,9 +105,16 @@ v-container#event.pa-0.pa-sm-2
|
|||
a(:href='resource.data.url || resource.data.context')
|
||||
small {{resource.data.published|dateFormat('ddd, D MMMM HH:mm')}}
|
||||
|
||||
v-card-text
|
||||
|
||||
div.mt-1(v-html='resource_filter(resource.data.content)')
|
||||
span.previewImage(@click='showResource(resource)')
|
||||
img(v-for='img in resource.data.attachment' :src='img.url')
|
||||
span(v-for='attachment in resource.data.attachment' :key='attachment.url')
|
||||
audio(v-if='isAudio(attachment)' controls)
|
||||
source(:src='attachment.url')
|
||||
v-img.cursorPointer(v-if='isImg(attachment)' :src='attachment.url' @click='showResource(resource)'
|
||||
max-height="250px"
|
||||
max-width="250px"
|
||||
contain :alt='attachment.name')
|
||||
|
||||
//- Next/prev arrow
|
||||
.text-center.mt-5.mb-5
|
||||
|
@ -110,7 +125,7 @@ v-container#event.pa-0.pa-sm-2
|
|||
:to='`/event/${event.next}`' :disabled='!event.next')
|
||||
v-icon mdi-arrow-right
|
||||
|
||||
v-dialog(v-model='showEmbed' width='1000px')
|
||||
v-dialog(v-model='showEmbed' width='700px' :fullscreen='$vuetify.breakpoint.xsOnly')
|
||||
EmbedEvent(:event='event' @close='showEmbed=false')
|
||||
|
||||
</template>
|
||||
|
@ -118,22 +133,27 @@ v-container#event.pa-0.pa-sm-2
|
|||
import { mapState } from 'vuex'
|
||||
import EventAdmin from './eventAdmin'
|
||||
import EmbedEvent from './embedEvent'
|
||||
import get from 'lodash/get'
|
||||
import moment from 'dayjs'
|
||||
import clipboard from '../../assets/clipboard'
|
||||
|
||||
const htmlToText = require('html-to-text')
|
||||
|
||||
export default {
|
||||
name: 'Event',
|
||||
mixins: [clipboard],
|
||||
components: { EventAdmin, EmbedEvent },
|
||||
async asyncData ({ $axios, params, error, store }) {
|
||||
try {
|
||||
const event = await $axios.$get(`/event/${params.id}`)
|
||||
return { event, id: Number(params.id) }
|
||||
const event = await $axios.$get(`/event/${params.slug}`)
|
||||
return { event }
|
||||
} catch (e) {
|
||||
error({ statusCode: 404, message: 'Event not found' })
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
currentAttachment: 0,
|
||||
event: {},
|
||||
showEmbed: false,
|
||||
showResources: false,
|
||||
|
@ -153,8 +173,8 @@ export default {
|
|||
const place_feed = {
|
||||
rel: 'alternate',
|
||||
type: 'application/rss+xml',
|
||||
title: `${this.settings.title} events @${this.event.place.name}`,
|
||||
href: this.settings.baseurl + `/feed/rss?places=${this.event.place.id}`
|
||||
title: `${this.settings.title} events @${this.event.place && this.event.place.name}`,
|
||||
href: this.settings.baseurl + `/feed/rss?places=${this.event.place && this.event.place.id}`
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -180,7 +200,7 @@ export default {
|
|||
{ property: 'og:type', content: 'event' },
|
||||
{
|
||||
property: 'og:image',
|
||||
content: this.thumbImgPath
|
||||
content: this.$options.filters.mediaURL(this.event)
|
||||
},
|
||||
{ property: 'og:site_name', content: this.settings.title },
|
||||
{
|
||||
|
@ -196,7 +216,7 @@ export default {
|
|||
{ property: 'twitter:title', content: this.event.title },
|
||||
{
|
||||
property: 'twitter:image',
|
||||
content: this.thumbImgPath
|
||||
content: this.$options.filters.mediaURL(this.event, 'thumb')
|
||||
},
|
||||
{
|
||||
property: 'twitter:description',
|
||||
|
@ -204,7 +224,7 @@ export default {
|
|||
}
|
||||
],
|
||||
link: [
|
||||
{ rel: 'image_src', href: this.thumbImgPath },
|
||||
{ rel: 'image_src', href: this.$options.filters.mediaURL(this.event, 'thumb') },
|
||||
{
|
||||
rel: 'alternate',
|
||||
type: 'application/rss+xml',
|
||||
|
@ -218,14 +238,14 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapState(['settings']),
|
||||
hasMedia () {
|
||||
return this.event.media && this.event.media.length
|
||||
},
|
||||
plainDescription () {
|
||||
return htmlToText.fromString(this.event.description.replace('\n', '').slice(0, 1000))
|
||||
},
|
||||
imgPath () {
|
||||
return '/media/' + this.event.image_path
|
||||
},
|
||||
thumbImgPath () {
|
||||
return this.settings.baseurl + '/media/thumb/' + this.event.image_path
|
||||
currentAttachmentLabel () {
|
||||
return get(this.selectedResource, `data.attachment[${this.currentAttachment}].name`, '')
|
||||
},
|
||||
is_mine () {
|
||||
if (!this.$auth.user) {
|
||||
|
@ -243,6 +263,14 @@ export default {
|
|||
window.removeEventListener('keydown', this.keyDown)
|
||||
},
|
||||
methods: {
|
||||
isImg (attachment) {
|
||||
const type = attachment.mediaType.split('/')[0]
|
||||
return type === 'image'
|
||||
},
|
||||
isAudio (attachment) {
|
||||
const type = attachment.mediaType.split('/')[0]
|
||||
return type === 'audio'
|
||||
},
|
||||
keyDown (ev) {
|
||||
if (ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey) { return }
|
||||
if (ev.key === 'ArrowRight' && this.event.next) {
|
||||
|
@ -255,7 +283,7 @@ export default {
|
|||
showResource (resource) {
|
||||
this.showResources = true
|
||||
this.selectedResource = resource
|
||||
document.getElementById('resourceDialog').focus()
|
||||
// document.getElementById('resourceDialog').focus()
|
||||
},
|
||||
async hideResource (resource, hidden) {
|
||||
await this.$axios.$put(`/resources/${resource.id}`, { hidden })
|
||||
|
@ -298,16 +326,9 @@ export default {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
<style lang='less'>
|
||||
.title {
|
||||
margin-bottom: 25px;
|
||||
color: yellow;
|
||||
font-weight: 300 !important;
|
||||
}
|
||||
<style scoped>
|
||||
.main_image {
|
||||
// width: 100%;
|
||||
margin: 0 auto;
|
||||
// max-height: 120vh;
|
||||
border-radius: 5px;
|
||||
transition: max-height 0.2s;
|
||||
}
|
|
@ -2,41 +2,32 @@
|
|||
v-card
|
||||
v-card-title(v-text="$t('common.embed_title')")
|
||||
v-card-text
|
||||
v-row
|
||||
v-col.col-12
|
||||
v-alert.mb-1.mt-1(type='info' show-icon) {{$t('common.embed_help')}}
|
||||
v-text-field(v-model='code')
|
||||
v-btn(slot='prepend' text color='primary'
|
||||
v-clipboard:copy='code'
|
||||
v-clipboard:success='copyLink') {{$t("common.copy")}}
|
||||
v-alert.mb-3.mt-1(type='info' show-icon) {{$t('common.embed_help')}}
|
||||
v-alert.pa-5.my-4.blue-grey.darken-4.text-body-1.lime--text.text--lighten-3 <pre>{{code}}</pre>
|
||||
v-btn.float-end(text color='primary' @click='clipboard(code)') {{$t("common.copy")}}
|
||||
v-icon.ml-1 mdi-content-copy
|
||||
|
||||
v-col.mt-2(v-html='code')
|
||||
p.mx-auto
|
||||
.mx-auto
|
||||
gancio-event(:id='event.id' :baseurl='settings.baseurl')
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(color='warning' @click="$emit('close')") {{$t("common.cancel")}}
|
||||
v-btn(v-clipboard:copy='code' v-clipboard:success='copyLink' color="primary") {{$t("common.copy")}}
|
||||
v-btn(text color='warning' @click="$emit('close')") {{$t("common.cancel")}}
|
||||
v-btn(text @click='clipboard(code)' color="primary") {{$t("common.copy")}}
|
||||
</template>
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import clipboard from '../../assets/clipboard'
|
||||
|
||||
export default {
|
||||
name: 'EmbedEvent',
|
||||
mixins: [clipboard],
|
||||
props: {
|
||||
event: { type: Object, default: () => ({}) }
|
||||
},
|
||||
computed: {
|
||||
...mapState(['settings']),
|
||||
code () {
|
||||
const style = "style='border: 0; width: 100%; height: 215px;'"
|
||||
const src = `${this.settings.baseurl}/embed/${this.event.slug || this.event.id}`
|
||||
const code = `<iframe ${style} src="${src}"></iframe>`
|
||||
return code
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyLink () {
|
||||
this.$root.$message('common.copied', { color: 'success' })
|
||||
return `<script src='${this.settings.baseurl}\/gancio-events.es.js'><\/script>\n<gancio-event baseurl='${this.settings.baseurl}' id=${this.event.id}></gancio-event>\n\n`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
v-col
|
||||
Search(
|
||||
:filters='filters'
|
||||
@update='updateFilters')
|
||||
@update='f => filters = f')
|
||||
v-tabs(v-model='type')
|
||||
|
||||
//- TOFIX
|
||||
|
@ -30,9 +30,7 @@
|
|||
v-card-text
|
||||
p(v-html='$t(`export.feed_description`)')
|
||||
v-text-field(v-model='link' readonly)
|
||||
v-btn(slot='prepend' text color='primary'
|
||||
v-clipboard:copy='link'
|
||||
v-clipboard:success='copyLink.bind(this, "feed")') {{$t("common.copy")}}
|
||||
v-btn(slot='prepend' text color='primary' @click='clipboard(link)') {{$t("common.copy")}}
|
||||
v-icon.ml-1 mdi-content-copy
|
||||
|
||||
v-tab ics/ical
|
||||
|
@ -41,8 +39,7 @@
|
|||
v-card-text
|
||||
p(v-html='$t(`export.ical_description`)')
|
||||
v-text-field(v-model='link')
|
||||
v-btn(slot='prepend' text color='primary'
|
||||
v-clipboard:copy='link' v-clipboard:success='copyLink.bind(this, "ical")') {{$t("common.copy")}}
|
||||
v-btn(slot='prepend' text color='primary' @click='clipboard(link)') {{$t("common.copy")}}
|
||||
v-icon.ml-1 mdi-content-copy
|
||||
|
||||
v-tab List
|
||||
|
@ -54,16 +51,17 @@
|
|||
v-row
|
||||
v-col.mr-2(:span='11')
|
||||
v-text-field(v-model='list.title' :label='$t("common.title")')
|
||||
v-text-field(v-model='list.maxEvents' type='number' :label='$t("common.max_events")')
|
||||
v-text-field(v-model='list.maxEvents' type='number' min='1' :label='$t("common.max_events")')
|
||||
v-col.float-right(:span='12')
|
||||
List(
|
||||
span {{filters.places.join(',')}}
|
||||
gancio-events(:baseurl='settings.baseurl'
|
||||
:maxlength='list.maxEvents && Number(list.maxEvents)'
|
||||
:title='list.title'
|
||||
:maxEvents='list.maxEvents'
|
||||
:events='events')
|
||||
v-text-field.mb-1(type='textarea' v-model='listScript' readonly )
|
||||
v-btn(slot='prepend' text
|
||||
color='primary' v-clipboard:copy='listScript' v-clipboard:success='copyLink.bind(this,"list")') {{$t('common.copy')}}
|
||||
v-icon.ml-1 mdi-content-copy
|
||||
:places='filters.places.join(",")'
|
||||
:tags='filters.tags.join(",")')
|
||||
v-alert.pa-5.my-4.blue-grey.darken-4.text-body-1.lime--text.text--lighten-3 <pre>{{code}}</pre>
|
||||
v-btn.float-end(text color='primary' @click='clipboard(code)') {{$t("common.copy")}}
|
||||
v-icon.ml-1 mdi-content-copy
|
||||
|
||||
v-tab(v-if='settings.enable_federation') {{$t('common.fediverse')}}
|
||||
v-tab-item(v-if='settings.enable_federation')
|
||||
|
@ -84,10 +82,12 @@ import { mapState } from 'vuex'
|
|||
import List from '@/components/List'
|
||||
import FollowMe from '../components/FollowMe'
|
||||
import Search from '@/components/Search'
|
||||
import clipboard from '../assets/clipboard'
|
||||
|
||||
export default {
|
||||
name: 'Exports',
|
||||
components: { List, FollowMe, Search },
|
||||
mixins: [clipboard],
|
||||
async asyncData ({ $axios, params, store, $api }) {
|
||||
const events = await $api.getEvents({
|
||||
start: dayjs().unix(),
|
||||
|
@ -99,7 +99,7 @@ export default {
|
|||
return {
|
||||
type: 'rss',
|
||||
notification: { email: '' },
|
||||
list: { title: 'Gancio', maxEvents: 3 },
|
||||
list: { title: 'Gancio', maxEvents: null },
|
||||
filters: { tags: [], places: [], show_recurrent: false },
|
||||
events: []
|
||||
}
|
||||
|
@ -111,28 +111,32 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapState(['settings']),
|
||||
domain () {
|
||||
const URL = url.parse(this.settings.baseurl)
|
||||
return URL.hostname
|
||||
},
|
||||
listScript () {
|
||||
const params = []
|
||||
code () {
|
||||
const params = [`baseurl="${this.settings.baseurl}"`]
|
||||
|
||||
if (this.list.title) {
|
||||
params.push(`title=${this.list.title}`)
|
||||
params.push(`title="${this.list.title}"`)
|
||||
}
|
||||
|
||||
if (this.filters.places.length) {
|
||||
params.push(`places=${this.filters.places.map(p => p.id)}`)
|
||||
params.push(`places="${this.filters.places.join(',')}"`)
|
||||
}
|
||||
|
||||
if (this.filters.tags.length) {
|
||||
params.push(`tags=${this.filters.tags.join(',')}`)
|
||||
params.push(`tags="${this.filters.tags.join(',')}"`)
|
||||
}
|
||||
|
||||
if (this.filters.show_recurrent) {
|
||||
params.push('show_recurrent=true')
|
||||
params.push('show_recurrent')
|
||||
}
|
||||
return `<iframe style='border: 0px; width: 100%;' src="${this.settings.baseurl}/embed/list?${params.join('&')}"></iframe>`
|
||||
|
||||
if (this.list.maxEvents) {
|
||||
params.push('maxlength=' + this.list.maxEvents)
|
||||
}
|
||||
|
||||
return `<script src="${this.settings.baseurl}\/gancio-events.es.js'><\/script>\n<gancio-events ${params.join(' ')}></gancio-events>\n\n`
|
||||
|
||||
|
||||
},
|
||||
link () {
|
||||
const typeMap = ['rss', 'ics', 'list']
|
||||
|
@ -157,22 +161,6 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
async updateFilters (filters) {
|
||||
this.filters = filters
|
||||
this.events = await this.$api.getEvents({
|
||||
start: dayjs().unix(),
|
||||
places: this.filters.places,
|
||||
tags: this.filters.tags,
|
||||
show_recurrent: !!this.filters.show_recurrent
|
||||
})
|
||||
},
|
||||
copyLink (type) {
|
||||
if (type === 'feed') {
|
||||
this.$root.$message('common.feed_url_copied')
|
||||
} else {
|
||||
this.$root.$message('common.copied')
|
||||
}
|
||||
},
|
||||
async add_notification () {
|
||||
// validate()
|
||||
// if (!this.notification.email) {
|
||||
|
@ -184,7 +172,7 @@ export default {
|
|||
// Message({ message: this.$t('email_notification_activated'), showClose: true, type: 'success' })
|
||||
},
|
||||
imgPath (event) {
|
||||
return event.image_path && event.image_path
|
||||
return event.media && event.media[0].url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
.col-xl-5.col-lg-5.col-md-7.col-sm-12.col-xs-12.pa-4.pa-sm-3
|
||||
//- this is needed as v-calendar does not support SSR
|
||||
//- https://github.com/nathanreyes/v-calendar/issues/336
|
||||
client-only
|
||||
client-only(placeholder='Calendar unavailable without js')
|
||||
Calendar(@dayclick='dayChange' @monthchange='monthChange' :events='filteredEvents')
|
||||
|
||||
.col.pt-0.pt-md-2
|
||||
|
@ -36,6 +36,7 @@ import Calendar from '@/components/Calendar'
|
|||
export default {
|
||||
name: 'Index',
|
||||
components: { Event, Search, Announcement, Calendar },
|
||||
middleware: 'setup',
|
||||
async asyncData ({ params, $api, store }) {
|
||||
const events = await $api.getEvents({
|
||||
start: dayjs().startOf('month').unix(),
|
||||
|
@ -65,7 +66,7 @@ export default {
|
|||
{ hid: 'og-description', name: 'og:description', content: this.settings.description },
|
||||
{ hid: 'og-title', property: 'og:title', content: this.settings.title },
|
||||
{ hid: 'og-url', property: 'og:url', content: this.settings.baseurl },
|
||||
{ property: 'og:image', content: this.settings.baseurl + '/favicon.ico' }
|
||||
{ property: 'og:image', content: this.settings.baseurl + '/logo.png' }
|
||||
],
|
||||
link: [
|
||||
{ rel: 'alternate', type: 'application/rss+xml', title: this.settings.title, href: this.settings.baseurl + '/feed/rss' }
|
||||
|
|
|
@ -1,33 +1,34 @@
|
|||
<template lang='pug'>
|
||||
v-container
|
||||
v-row.mt-5(align='center' justify='center')
|
||||
v-col(cols='12' md="6" lg="5" xl="4")
|
||||
v-card
|
||||
v-card-title {{settings.title}} - {{$t('common.recover_password')}}
|
||||
v-card-text
|
||||
div(v-if='valid')
|
||||
v-card-title {{$t('common.recover_password')}}
|
||||
template(v-if='user')
|
||||
v-card-subtitle {{user.email}}
|
||||
v-card-text
|
||||
v-text-field(type='password'
|
||||
:rules="$validators.password"
|
||||
autofocus :placeholder='$t("common.new_password")'
|
||||
v-model='new_password')
|
||||
div(v-else) {{$t('recover.not_valid_code')}}
|
||||
div(v-else) {{$t('recover.not_valid_code')}}
|
||||
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(v-if='valid' color='primary' @click='change_password') {{$t('common.send')}}
|
||||
v-btn(v-if='user' text color='primary' @click='change_password') {{$t('common.send')}}
|
||||
</template>
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'Recover',
|
||||
layout: 'modal',
|
||||
async asyncData ({ params, $axios }) {
|
||||
const code = params.code
|
||||
try {
|
||||
const valid = await $axios.$post('/user/check_recover_code', { recover_code: code })
|
||||
return { valid, code }
|
||||
const user = await $axios.$post('/user/check_recover_code', { recover_code: code })
|
||||
return { user, code }
|
||||
} catch (e) {
|
||||
return { valid: false }
|
||||
return { user: false }
|
||||
}
|
||||
},
|
||||
data () {
|
||||
|
@ -50,7 +51,7 @@ export default {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
<style lang='less'>
|
||||
<style>
|
||||
h4 img {
|
||||
max-height: 40px;
|
||||
border-radius: 20px;
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
</template>
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import url from 'url'
|
||||
|
||||
export default {
|
||||
name: 'Settings',
|
||||
|
@ -19,12 +18,7 @@ export default {
|
|||
user: { }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['settings']),
|
||||
baseurl () {
|
||||
return url.parse(this.settings.baseurl).host
|
||||
}
|
||||
},
|
||||
computed: mapState(['settings']),
|
||||
methods: {
|
||||
// async change_password () {
|
||||
// if (!this.password) { return }
|
||||
|
|
38
pages/setup/Completed.vue
Normal file
38
pages/setup/Completed.vue
Normal file
|
@ -0,0 +1,38 @@
|
|||
<template lang="pug">
|
||||
v-container
|
||||
v-card-title.d-block.text-h5.text-center(v-text="$t('setup.completed')")
|
||||
v-card-text(v-html="$t('setup.completed_description', user)")
|
||||
v-card-actions
|
||||
v-btn(text @click='next' color='primary' :loading='loading' :disabled='loading') {{$t('setup.start')}}
|
||||
v-icon mdi-arrow-right
|
||||
</template>
|
||||
<script>
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
user: {
|
||||
email: 'admin',
|
||||
password: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
next () {
|
||||
window.location='/admin'
|
||||
},
|
||||
async start (user) {
|
||||
this.user = { ...user }
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
await this.$axios.$get('/ping')
|
||||
this.loading = false
|
||||
} catch (e) {
|
||||
setTimeout(() => this.start(user), 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
47
pages/setup/DbStep.vue
Normal file
47
pages/setup/DbStep.vue
Normal file
|
@ -0,0 +1,47 @@
|
|||
<template lang="pug">
|
||||
v-container
|
||||
v-card-title.text-h5 Database
|
||||
v-card-text
|
||||
v-form
|
||||
v-btn-toggle(text color='primary' v-model='db.dialect')
|
||||
v-btn(value='sqlite' text) sqlite
|
||||
v-btn(value='postgres' text) postgres
|
||||
template(v-if='db.dialect === "sqlite"')
|
||||
v-text-field(v-model='db.storage' label='Path')
|
||||
template(v-if='db.dialect === "postgres"')
|
||||
v-text-field(v-model='db.hostname' label='Hostname' :rules="[$validators.required('hostname')]")
|
||||
v-text-field(v-model='db.database' label='Database' :rules="[$validators.required('database')]")
|
||||
v-text-field(v-model='db.username' label='Username' :rules="[$validators.required('username')]")
|
||||
v-text-field(type='password' v-model='db.password' label='Password' :rules="[$validators.required('password')]")
|
||||
|
||||
v-card-actions
|
||||
v-btn(text @click='checkDb' color='primary' :loading='loading' :disabled='loading') {{$t('common.next')}}
|
||||
v-icon mdi-arrow-right
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
db: {
|
||||
storage: './gancio.sqlite',
|
||||
hostname: 'localhost',
|
||||
database: 'gancio'
|
||||
},
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async checkDb () {
|
||||
this.loading = true
|
||||
try {
|
||||
await this.$axios.$post('/setup/db', { db: this.db })
|
||||
this.$root.$message('DB Connection OK!', { color: 'success' })
|
||||
this.$emit('complete', this.db)
|
||||
} catch (e) {
|
||||
this.$root.$message(e.response.data, { color: 'error' })
|
||||
}
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
64
pages/setup/index.vue
Normal file
64
pages/setup/index.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<template lang="pug">
|
||||
|
||||
v-container.pa-6
|
||||
h2.mb-2.text-center Gancio Setup
|
||||
v-stepper.grey.lighten-5(v-model='step')
|
||||
v-stepper-header
|
||||
v-stepper-step(:complete='step > 1' step='1') Database
|
||||
v-divider
|
||||
v-stepper-step(:complete='step > 2' step='2') Configuration
|
||||
v-divider
|
||||
v-stepper-step(:complete='step > 3' step='3') Finish
|
||||
|
||||
v-stepper-items
|
||||
v-stepper-content(step='1')
|
||||
DbStep(@complete='dbCompleted')
|
||||
v-stepper-content(step='2')
|
||||
Settings(setup, @complete='configCompleted')
|
||||
v-stepper-content(step='3')
|
||||
Completed(ref='completed')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import DbStep from './DbStep'
|
||||
import Settings from '../../components/admin/Settings'
|
||||
import Completed from './Completed'
|
||||
|
||||
export default {
|
||||
components: { DbStep, Settings, Completed },
|
||||
middleware: 'setup',
|
||||
layout: 'clean',
|
||||
head: {
|
||||
title: 'Setup',
|
||||
},
|
||||
auth: false,
|
||||
data () {
|
||||
return {
|
||||
config: {
|
||||
db: {
|
||||
dialect: ''
|
||||
}
|
||||
},
|
||||
step: 1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
dbCompleted (db) {
|
||||
this.step = this.step + 1
|
||||
},
|
||||
async configCompleted () {
|
||||
try {
|
||||
const user = await this.$axios.$post('/setup/restart')
|
||||
this.step = this.step + 1
|
||||
this.$refs.completed.start(user)
|
||||
} catch (e) {
|
||||
this.$root.$message(e.response ? e.response.data : e, { color: 'error' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,17 +1,21 @@
|
|||
<template lang="pug">
|
||||
v-container
|
||||
v-row.mt-5(align='center' justify='center')
|
||||
v-col(cols='12' md="6" lg="5" xl="4")
|
||||
v-card
|
||||
v-card-title <nuxt-link to='/'><img src='/favicon.ico'/></nuxt-link> {{$t('common.set_password')}}
|
||||
template(v-if='valid')
|
||||
v-card-text(v-if='valid')
|
||||
v-form(v-if='valid')
|
||||
v-text-field(type='password' v-model='new_password' :label="$t('common.new_password')")
|
||||
v-card-title {{$t('common.set_password')}}
|
||||
template(v-if='user')
|
||||
v-card-subtitle {{user.email}}
|
||||
v-card-text
|
||||
v-form
|
||||
v-text-field(type='password' v-model='new_password' :label="$t('common.new_password')" :rules='$validators.password' autofocus)
|
||||
|
||||
v-card-actions
|
||||
v-btn(color="success" :disabled='!new_password' @click='change_password') {{$t('common.send')}}
|
||||
v-spacer
|
||||
v-btn(text color="primary" :disabled='!new_password' @click='change_password') {{$t('common.send')}}
|
||||
|
||||
v-card-text(v-else) {{$t('recover.not_valid_code')}}
|
||||
v-card-text(v-else)
|
||||
v-alert.ma-5(type='error') {{$t('recover.not_valid_code')}}
|
||||
|
||||
</template>
|
||||
<script>
|
||||
|
@ -21,10 +25,10 @@ export default {
|
|||
async asyncData ({ params, $axios }) {
|
||||
const code = params.code
|
||||
try {
|
||||
const valid = await $axios.$post('/user/check_recover_code', { recover_code: code })
|
||||
return { valid, code }
|
||||
const user = await $axios.$post('/user/check_recover_code', { recover_code: code })
|
||||
return { user, code }
|
||||
} catch (e) {
|
||||
return { valid: false }
|
||||
return { user: false }
|
||||
}
|
||||
},
|
||||
data () {
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
<template lang="pug">
|
||||
el-card
|
||||
nuxt-link.float-right(to='/')
|
||||
el-button(circle icon='el-icon-close' type='danger' size='small' plain)
|
||||
|
||||
h5 <img src='/favicon.ico'/> {{$t('common.set_password')}}
|
||||
div(v-if='valid')
|
||||
el-form
|
||||
el-form-item {{$t('common.new_password')}}
|
||||
el-input(type='password', v-model='new_password')
|
||||
el-button(plain type="success" icon='el-icon-send', @click='change_password') {{$t('common.send')}}
|
||||
|
||||
div(v-else) {{$t('recover.not_valid_code')}}
|
||||
|
||||
</template>
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Recover',
|
||||
async asyncData ({ params, $axios }) {
|
||||
const code = params.code
|
||||
try {
|
||||
const valid = await $axios.$post('/user/check_recover_code', { recover_code: code })
|
||||
return { valid, code }
|
||||
} catch (e) {
|
||||
return { valid: false }
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return { new_password: '' }
|
||||
},
|
||||
methods: {
|
||||
async change_password () {
|
||||
try {
|
||||
await this.$axios.$post('/user/recover_password', { recover_code: this.code, password: this.new_password })
|
||||
this.$root.$message('common.password_updated', { color: 'succes' })
|
||||
this.$router.replace('/login')
|
||||
} catch (e) {
|
||||
this.$root.$message(e, { color: 'warning' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
export default ({ $axios, store }, inject) => {
|
||||
export default ({ $axios }, inject) => {
|
||||
const api = {
|
||||
|
||||
/**
|
||||
|
|
|
@ -33,6 +33,18 @@ export default ({ app, store }) => {
|
|||
|
||||
// shown in mobile homepage
|
||||
Vue.filter('day', value => dayjs.unix(value).locale(store.state.locale).format('dddd, D MMM'))
|
||||
Vue.filter('mediaURL', (event, type) => {
|
||||
if (event.media && event.media.length) {
|
||||
if (type === 'alt') {
|
||||
return event.media[0].name
|
||||
} else {
|
||||
return store.state.settings.baseurl + '/media/' + (type === 'thumb' ? 'thumb/' : '') + event.media[0].url
|
||||
}
|
||||
} else if (type !== 'alt') {
|
||||
return store.state.settings.baseurl + '/media/' + (type === 'thumb' ? 'thumb/' : '') + 'logo.svg'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
Vue.filter('from', timestamp => dayjs.unix(timestamp).fromNow())
|
||||
|
||||
|
|
28
plugins/gancioPluginExample.js
Normal file
28
plugins/gancioPluginExample.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
|
||||
const plugin = {
|
||||
gancio: null,
|
||||
load (gancio) {
|
||||
console.error('Plugin GancioPluginExample loaded!')
|
||||
plugin.gancio = gancio
|
||||
},
|
||||
|
||||
onEventCreate (event) {
|
||||
const eventLink = `${plugin.gancio.settings.baseurl}/event/${event.slug}`
|
||||
if (!event.is_visible) {
|
||||
console.error(`Unconfirmed event created: ${event.title} / ${eventLink}`)
|
||||
} else {
|
||||
console.error(`Event created: ${event.title} / ${eventLink}`)
|
||||
}
|
||||
},
|
||||
|
||||
onEventUpdate (event) {
|
||||
console.error(`Event "${event.title}" updated`)
|
||||
},
|
||||
|
||||
onEventDelete (event) {
|
||||
console.error(`Event "${event.title}" deleted`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = plugin
|
|
@ -4,18 +4,20 @@ import merge from 'lodash/merge'
|
|||
|
||||
Vue.use(VueI18n)
|
||||
|
||||
export default ({ app, store, req }) => {
|
||||
export default async ({ app, store, req }) => {
|
||||
const messages = {}
|
||||
if (process.server) {
|
||||
store.commit('setLocale', req.settings.locale)
|
||||
if (req.settings.user_locale) { store.commit('setUserLocale', req.settings.user_locale) }
|
||||
store.commit('setLocale', req.acceptedLocale)
|
||||
if (req.user_locale) {
|
||||
store.commit('setUserLocale', req.user_locale)
|
||||
}
|
||||
}
|
||||
|
||||
const messages = {}
|
||||
messages[store.state.locale] = require(`../locales/${store.state.locale}.json`)
|
||||
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 = require('../locales/en.json')
|
||||
messages.en = await import('../locales/en.json')
|
||||
}
|
||||
|
||||
if (store.state.user_locale) {
|
||||
|
|
|
@ -8,7 +8,7 @@ export default ({ app }, inject) => {
|
|||
},
|
||||
email: [
|
||||
v => !!v || $t('validators.required', { fieldName: $t('common.email') }),
|
||||
v => (v && !!linkify.test(v, 'email')) || $t('validators.email')
|
||||
v => (v && (v === 'admin' || !!linkify.test(v, 'email')) || $t('validators.email'))
|
||||
],
|
||||
password: [
|
||||
v => !!v || $t('validators.required', { fieldName: $t('common.password') })
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
import VueClipboard from 'vue-clipboard2'
|
||||
|
||||
export default () => {
|
||||
Vue.use(VueClipboard)
|
||||
}
|
34
plugins/vuetify.js
Normal file
34
plugins/vuetify.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import Vue from 'vue'
|
||||
import Vuetify from 'vuetify'
|
||||
|
||||
// import it from 'vuetify/lib/locale/it.js'
|
||||
// import en from 'vuetify/lib/locale/en.js'
|
||||
// import es from 'vuetify/lib/locale/es'
|
||||
// import no from 'vuetify/lib/locale/no'
|
||||
// import fr from 'vuetify/lib/locale/fr'
|
||||
// import ca from 'vuetify/lib/locale/ca'
|
||||
|
||||
|
||||
export default ({ app }) => {
|
||||
Vue.use(Vuetify)
|
||||
app.vuetify = new Vuetify({
|
||||
// lang: {
|
||||
// locales: { en, it }, //, es, fr, no, ca },
|
||||
// current: 'en'
|
||||
// },
|
||||
icons: {
|
||||
iconfont: 'mdi'
|
||||
},
|
||||
theme: {
|
||||
dark: true,
|
||||
themes: {
|
||||
dark: {
|
||||
primary: '#FF6E40'
|
||||
},
|
||||
light: {
|
||||
primary: '#FF4500'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -17,7 +17,7 @@ const announceController = {
|
|||
announcement: req.body.announcement,
|
||||
visible: true
|
||||
}
|
||||
log.info('Create announcement: "%s" ', req.body.title)
|
||||
log.info('Create announcement: ' + req.body.title)
|
||||
const announce = await Announcement.create(announcementDetail)
|
||||
res.json(announce)
|
||||
},
|
||||
|
@ -34,20 +34,20 @@ const announceController = {
|
|||
announce = await announce.update(announceDetails)
|
||||
res.json(announce)
|
||||
} catch (e) {
|
||||
log.error('Toggle announcement failed: %s ', e)
|
||||
log.error('Toggle announcement failed', e)
|
||||
res.sendStatus(404)
|
||||
}
|
||||
},
|
||||
|
||||
async remove (req, res) {
|
||||
log.info('Remove announcement "%d"', req.params.announce_id)
|
||||
log.info('Remove announcement', req.params.announce_id)
|
||||
const announce_id = req.params.announce_id
|
||||
try {
|
||||
const announce = await Announcement.findByPk(announce_id)
|
||||
await announce.destroy()
|
||||
res.sendStatus(200)
|
||||
} catch (e) {
|
||||
log.error('Remove announcement failed: "%s" ', e)
|
||||
log.error('Remove announcement failed:', e)
|
||||
res.sendStatus(404)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
const crypto = require('crypto')
|
||||
const path = require('path')
|
||||
const config = require('config')
|
||||
const config = require('../../config')
|
||||
const fs = require('fs')
|
||||
const { Op } = require('sequelize')
|
||||
const intersection = require('lodash/intersection')
|
||||
const linkifyHtml = require('linkifyjs/html')
|
||||
const linkifyHtml = require('linkify-html')
|
||||
const Sequelize = require('sequelize')
|
||||
const dayjs = require('dayjs')
|
||||
const helpers = require('../../helpers')
|
||||
|
@ -85,11 +85,26 @@ const eventController = {
|
|||
res.json(place)
|
||||
},
|
||||
|
||||
async _get(slug) {
|
||||
// retrocompatibility, old events URL does not use slug, use id as fallback
|
||||
const id = Number(slug) || -1
|
||||
return Event.findOne({
|
||||
where: {
|
||||
[Op.or]: {
|
||||
slug,
|
||||
id
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async get (req, res) {
|
||||
const format = req.params.format || 'json'
|
||||
const is_admin = req.user && req.user.is_admin
|
||||
const slug = req.params.event_id
|
||||
const id = Number(req.params.event_id) || -1
|
||||
const slug = req.params.event_slug
|
||||
|
||||
// retrocompatibility, old events URL does not use slug, use id as fallback
|
||||
const id = Number(slug) || -1
|
||||
let event
|
||||
|
||||
try {
|
||||
|
@ -118,7 +133,7 @@ const eventController = {
|
|||
order: [[Resource, 'id', 'DESC']]
|
||||
})
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
log.error('[EVENT]', e)
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
|
@ -194,7 +209,7 @@ const eventController = {
|
|||
const notifier = require('../../notifier')
|
||||
notifier.notifyEvent('Create', event.id)
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
log.error('[EVENT]', e)
|
||||
res.sendStatus(404)
|
||||
}
|
||||
},
|
||||
|
@ -264,7 +279,7 @@ const eventController = {
|
|||
async add (req, res) {
|
||||
// req.err comes from multer streaming error
|
||||
if (req.err) {
|
||||
log.info(req.err)
|
||||
log.warn(req.err)
|
||||
return res.status(400).json(req.err.toString())
|
||||
}
|
||||
|
||||
|
@ -272,6 +287,11 @@ const eventController = {
|
|||
const body = req.body
|
||||
const recurrent = body.recurrent ? JSON.parse(body.recurrent) : null
|
||||
|
||||
if (!body.place_name) {
|
||||
log.warn('Place is required')
|
||||
return res.status(400).send('Place is required')
|
||||
}
|
||||
|
||||
const eventDetails = {
|
||||
title: body.title,
|
||||
// remove html tags
|
||||
|
@ -284,10 +304,23 @@ const eventController = {
|
|||
is_visible: !!req.user
|
||||
}
|
||||
|
||||
if (req.file) {
|
||||
eventDetails.image_path = req.file.filename
|
||||
} else if (body.image_url) {
|
||||
eventDetails.image_path = await helpers.getImageFromURL(body.image_url)
|
||||
if (req.file || body.image_url) {
|
||||
let url
|
||||
if (req.file) {
|
||||
url = req.file.filename
|
||||
} else {
|
||||
url = await helpers.getImageFromURL(body.image_url)
|
||||
}
|
||||
|
||||
let focalpoint = body.image_focalpoint ? body.image_focalpoint.split(',') : ['0', '0']
|
||||
focalpoint = [parseFloat(focalpoint[0]).toFixed(2), parseFloat(focalpoint[1]).toFixed(2)]
|
||||
eventDetails.media = [{
|
||||
url,
|
||||
name: body.image_name || body.title || '',
|
||||
focalpoint: [parseFloat(focalpoint[0]), parseFloat(focalpoint[1])]
|
||||
}]
|
||||
} else {
|
||||
eventDetails.media = []
|
||||
}
|
||||
|
||||
const event = await Event.create(eventDetails)
|
||||
|
@ -324,12 +357,12 @@ const eventController = {
|
|||
if (event.recurrent) {
|
||||
eventController._createRecurrent()
|
||||
} else {
|
||||
// send notifications (mastodon / email)
|
||||
// send notifications
|
||||
const notifier = require('../../notifier')
|
||||
notifier.notifyEvent('Create', event.id)
|
||||
}
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
log.error('[EVENT ADD]', e)
|
||||
res.sendStatus(400)
|
||||
}
|
||||
},
|
||||
|
@ -338,28 +371,29 @@ const eventController = {
|
|||
if (req.err) {
|
||||
return res.status(400).json(req.err.toString())
|
||||
}
|
||||
const body = req.body
|
||||
const event = await Event.findByPk(body.id)
|
||||
if (!event) { return res.sendStatus(404) }
|
||||
if (!req.user.is_admin && event.userId !== req.user.id) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
const recurrent = body.recurrent ? JSON.parse(body.recurrent) : null
|
||||
const eventDetails = {
|
||||
title: body.title,
|
||||
// remove html tags
|
||||
description: helpers.sanitizeHTML(linkifyHtml(body.description, { target: '_blank' })),
|
||||
multidate: body.multidate,
|
||||
start_datetime: body.start_datetime,
|
||||
end_datetime: body.end_datetime,
|
||||
recurrent
|
||||
}
|
||||
try {
|
||||
const body = req.body
|
||||
const event = await Event.findByPk(body.id)
|
||||
if (!event) { return res.sendStatus(404) }
|
||||
if (!req.user.is_admin && event.userId !== req.user.id) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
if (req.file) {
|
||||
if (event.image_path && !event.recurrent) {
|
||||
const old_path = path.resolve(config.upload_path, event.image_path)
|
||||
const old_thumb_path = path.resolve(config.upload_path, 'thumb', event.image_path)
|
||||
const recurrent = body.recurrent ? JSON.parse(body.recurrent) : null
|
||||
const eventDetails = {
|
||||
title: body.title,
|
||||
// remove html tags
|
||||
description: helpers.sanitizeHTML(linkifyHtml(body.description, { target: '_blank' })),
|
||||
multidate: body.multidate,
|
||||
start_datetime: body.start_datetime,
|
||||
end_datetime: body.end_datetime,
|
||||
recurrent
|
||||
}
|
||||
|
||||
if ((req.file || /^https?:\/\//.test(body.image_url)) && !event.recurrent && event.media && event.media.length) {
|
||||
const old_path = path.resolve(config.upload_path, event.media[0].url)
|
||||
const old_thumb_path = path.resolve(config.upload_path, 'thumb', event.media[0].url)
|
||||
try {
|
||||
fs.unlinkSync(old_path)
|
||||
fs.unlinkSync(old_thumb_path)
|
||||
|
@ -367,34 +401,55 @@ const eventController = {
|
|||
log.info(e.toString())
|
||||
}
|
||||
}
|
||||
eventDetails.image_path = req.file.filename
|
||||
} else if (body.image_url) {
|
||||
eventDetails.image_path = await helpers.getImageFromURL(body.image_url)
|
||||
}
|
||||
let url
|
||||
if (req.file) {
|
||||
url = req.file.filename
|
||||
} else if (body.image_url) {
|
||||
if (/^https?:\/\//.test(body.image_url)) {
|
||||
url = await helpers.getImageFromURL(body.image_url)
|
||||
} else {
|
||||
url = body.image_url
|
||||
}
|
||||
}
|
||||
|
||||
await event.update(eventDetails)
|
||||
const [place] = await Place.findOrCreate({
|
||||
where: { name: body.place_name },
|
||||
defaults: { address: body.place_address }
|
||||
})
|
||||
if (url && !event.recurrent) {
|
||||
const focalpoint = body.image_focalpoint ? body.image_focalpoint.split(',') : ['0', '0']
|
||||
eventDetails.media = [{
|
||||
url,
|
||||
name: body.image_name || '',
|
||||
focalpoint: [parseFloat(focalpoint[0].slice(0, 6)), parseFloat(focalpoint[1].slice(0, 6))]
|
||||
}]
|
||||
} else {
|
||||
eventDetails.media = []
|
||||
}
|
||||
|
||||
await event.setPlace(place)
|
||||
await event.setTags([])
|
||||
if (body.tags) {
|
||||
await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true })
|
||||
const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } })
|
||||
await event.addTags(tags)
|
||||
}
|
||||
const newEvent = await Event.findByPk(event.id, { include: [Tag, Place] })
|
||||
res.json(newEvent)
|
||||
await event.update(eventDetails)
|
||||
const [place] = await Place.findOrCreate({
|
||||
where: { name: body.place_name },
|
||||
defaults: { address: body.place_address }
|
||||
})
|
||||
|
||||
// create recurrent instances of event if needed
|
||||
// without waiting for the task manager
|
||||
if (event.recurrent) {
|
||||
eventController._createRecurrent()
|
||||
} else {
|
||||
const notifier = require('../../notifier')
|
||||
notifier.notifyEvent('Update', event.id)
|
||||
await event.setPlace(place)
|
||||
await event.setTags([])
|
||||
if (body.tags) {
|
||||
await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true })
|
||||
const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } })
|
||||
await event.addTags(tags)
|
||||
}
|
||||
const newEvent = await Event.findByPk(event.id, { include: [Tag, Place] })
|
||||
res.json(newEvent)
|
||||
|
||||
// create recurrent instances of event if needed
|
||||
// without waiting for the task manager
|
||||
if (event.recurrent) {
|
||||
eventController._createRecurrent()
|
||||
} else {
|
||||
const notifier = require('../../notifier')
|
||||
notifier.notifyEvent('Update', event.id)
|
||||
}
|
||||
} catch (e) {
|
||||
log.error('[EVENT UPDATE]', e)
|
||||
res.sendStatus(400)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -402,9 +457,9 @@ const eventController = {
|
|||
const event = await Event.findByPk(req.params.id)
|
||||
// check if event is mine (or user is admin)
|
||||
if (event && (req.user.is_admin || req.user.id === event.userId)) {
|
||||
if (event.image_path && !event.recurrent) {
|
||||
const old_path = path.join(config.upload_path, event.image_path)
|
||||
const old_thumb_path = path.join(config.upload_path, 'thumb', event.image_path)
|
||||
if (event.media && event.media.length && !event.recurrent) {
|
||||
const old_path = path.join(config.upload_path, event.media[0].url)
|
||||
const old_thumb_path = path.join(config.upload_path, 'thumb', event.media[0].url)
|
||||
try {
|
||||
fs.unlinkSync(old_thumb_path)
|
||||
fs.unlinkSync(old_path)
|
||||
|
@ -414,6 +469,12 @@ const eventController = {
|
|||
}
|
||||
const notifier = require('../../notifier')
|
||||
await notifier.notifyEvent('Delete', event.id)
|
||||
|
||||
// unassociate child events
|
||||
if (event.recurrent) {
|
||||
await Event.update({ parentId: null }, { where: { parentId: event.id } })
|
||||
}
|
||||
log.debug('[EVENT REMOVED] ' + event.title)
|
||||
await event.destroy()
|
||||
res.sendStatus(200)
|
||||
} else {
|
||||
|
@ -421,7 +482,8 @@ const eventController = {
|
|||
}
|
||||
},
|
||||
|
||||
async _select ({ start, end, tags, places, show_recurrent }) {
|
||||
async _select ({ start, end, tags, places, show_recurrent, max }) {
|
||||
|
||||
const where = {
|
||||
// do not include parent recurrent event
|
||||
recurrent: null,
|
||||
|
@ -448,7 +510,7 @@ const eventController = {
|
|||
|
||||
let where_tags = {}
|
||||
if (tags) {
|
||||
where_tags = { where: { tag: tags.split(',') } }
|
||||
where_tags = { where: { [Op.or]: { tag: tags.split(',') } } }
|
||||
}
|
||||
|
||||
const events = await Event.findAll({
|
||||
|
@ -456,20 +518,23 @@ const eventController = {
|
|||
attributes: {
|
||||
exclude: ['likes', 'boost', 'userId', 'is_visible', 'createdAt', 'updatedAt', 'description', 'resources']
|
||||
},
|
||||
order: ['start_datetime', Sequelize.literal('(SELECT COUNT("tagTag") FROM event_tags WHERE "tagTag" = tag) DESC')],
|
||||
order: ['start_datetime'],
|
||||
include: [
|
||||
{ model: Resource, required: false, attributes: ['id'] },
|
||||
{
|
||||
model: Tag,
|
||||
order: [Sequelize.literal('(SELECT COUNT("tagTag") FROM event_tags WHERE tagTag = tag) DESC')],
|
||||
attributes: ['tag'],
|
||||
required: !!tags,
|
||||
...where_tags,
|
||||
through: { attributes: [] }
|
||||
},
|
||||
{ model: Place, required: true, attributes: ['id', 'name', 'address'] }
|
||||
]
|
||||
],
|
||||
limit: max
|
||||
}).catch(e => {
|
||||
log.error(e)
|
||||
log.error('[EVENT]', e)
|
||||
return []
|
||||
})
|
||||
|
||||
return events.map(e => {
|
||||
|
@ -483,20 +548,22 @@ const eventController = {
|
|||
* Select events based on params
|
||||
*/
|
||||
async select (req, res) {
|
||||
const start = req.query.start
|
||||
const start = req.query.start || dayjs().unix()
|
||||
const end = req.query.end
|
||||
const tags = req.query.tags
|
||||
const places = req.query.places
|
||||
const max = req.query.max
|
||||
const show_recurrent = settingsController.settings.allow_recurrent_event &&
|
||||
(typeof req.query.show_recurrent !== 'undefined' ? req.query.show_recurrent === 'true' : settingsController.settings.recurrent_event_visible)
|
||||
|
||||
res.json(await eventController._select({
|
||||
start, end, places, tags, show_recurrent
|
||||
start, end, places, tags, show_recurrent, max
|
||||
}))
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensure we have the next instance of a recurrent event
|
||||
* TODO: create a future instance if the next one is skipped
|
||||
*/
|
||||
_createRecurrentOccurrence (e) {
|
||||
log.debug(`Create recurrent event [${e.id}] ${e.title}"`)
|
||||
|
@ -504,15 +571,16 @@ const eventController = {
|
|||
parentId: e.id,
|
||||
title: e.title,
|
||||
description: e.description,
|
||||
image_path: e.image_path,
|
||||
media: e.media,
|
||||
is_visible: true,
|
||||
userId: e.userId,
|
||||
placeId: e.placeId
|
||||
}
|
||||
|
||||
const recurrent = e.recurrent
|
||||
let cursor = dayjs()
|
||||
const start_date = dayjs.unix(e.start_datetime)
|
||||
const now = dayjs()
|
||||
let cursor = start_date > now ? start_date : now
|
||||
const duration = dayjs.unix(e.end_datetime).diff(start_date, 's')
|
||||
const frequency = recurrent.frequency
|
||||
const type = recurrent.type
|
||||
|
|
|
@ -12,6 +12,7 @@ const exportController = {
|
|||
const type = req.params.type
|
||||
const tags = req.query.tags
|
||||
const places = req.query.places
|
||||
const show_recurrent = !!req.query.show_recurrent
|
||||
|
||||
const where = {}
|
||||
const yesterday = moment().subtract('1', 'day').unix()
|
||||
|
@ -25,9 +26,13 @@ const exportController = {
|
|||
where.placeId = places.split(',')
|
||||
}
|
||||
|
||||
if (!show_recurrent) {
|
||||
where.parentId = null
|
||||
}
|
||||
|
||||
const events = await Event.findAll({
|
||||
order: ['start_datetime'],
|
||||
attributes: { exclude: ['is_visible', 'recurrent', 'createdAt', 'updatedAt', 'likes', 'boost', 'slug', 'userId', 'placeId'] },
|
||||
attributes: { exclude: ['is_visible', 'recurrent', 'createdAt', 'likes', 'boost', 'userId', 'placeId'] },
|
||||
where: {
|
||||
is_visible: true,
|
||||
recurrent: { [Op.eq]: null },
|
||||
|
@ -62,8 +67,8 @@ const exportController = {
|
|||
const eventsMap = events.map(e => {
|
||||
const tmpStart = moment.unix(e.start_datetime)
|
||||
const tmpEnd = moment.unix(e.end_datetime)
|
||||
const start = tmpStart.utc(true).format('YYYY-M-D-H-m').split('-')
|
||||
const end = tmpEnd.utc(true).format('YYYY-M-D-H-m').split('-')
|
||||
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 {
|
||||
start,
|
||||
// startOutputType: 'utc',
|
||||
|
@ -77,8 +82,12 @@ const exportController = {
|
|||
}
|
||||
})
|
||||
res.type('text/calendar; charset=UTF-8')
|
||||
const ret = ics.createEvents(eventsMap)
|
||||
res.send(ret.value)
|
||||
ics.createEvents(eventsMap, (err, value) => {
|
||||
if (err) {
|
||||
return res.status(401).send(err)
|
||||
}
|
||||
return res.send(value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ const oauthController = {
|
|||
delete client.id
|
||||
res.json(client)
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
log.error('[OAUTH CLIENT]', e)
|
||||
res.status(400).json(e)
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
const Resource = require('../models/resource')
|
||||
const APUser = require('../models/ap_user')
|
||||
const Event = require('../models/event')
|
||||
const get = require('lodash/get')
|
||||
|
||||
const resourceController = {
|
||||
async hide (req, res) {
|
||||
|
@ -17,12 +20,28 @@ const resourceController = {
|
|||
},
|
||||
|
||||
async getAll (req, res) {
|
||||
const limit = req.body.limit || 100
|
||||
const limit = req.body.limit || 1000
|
||||
// const where = {}
|
||||
// if (req.params.instanceId) {
|
||||
// where =
|
||||
//
|
||||
const resources = await Resource.findAll({ limit })
|
||||
let resources = await Resource.findAll({ limit, include: [APUser, Event], order: [['createdAt', 'DESC']] })
|
||||
resources = resources.map(r => ({
|
||||
id: r.id,
|
||||
hidden: r.hidden,
|
||||
created: r.createdAt,
|
||||
data: {
|
||||
content: r.data.content
|
||||
},
|
||||
event: {
|
||||
id: r.event.id,
|
||||
title: r.event.title
|
||||
},
|
||||
ap_user: {
|
||||
ap_id: get(r, 'ap_user.ap_id', ''),
|
||||
preferredUsername: get(r, 'ap_user.object.preferredUsername', '')
|
||||
}
|
||||
}))
|
||||
res.json(resources)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,29 @@
|
|||
const Setting = require('../models/setting')
|
||||
const config = require('config')
|
||||
const consola = require('consola')
|
||||
const path = require('path')
|
||||
const URL = require('url')
|
||||
const fs = require('fs')
|
||||
const pkg = require('../../../package.json')
|
||||
const crypto = require('crypto')
|
||||
const util = require('util')
|
||||
const toIco = require('to-ico')
|
||||
const generateKeyPair = util.promisify(crypto.generateKeyPair)
|
||||
const readFile = util.promisify(fs.readFile)
|
||||
const writeFile = util.promisify(fs.writeFile)
|
||||
const { promisify } = require('util')
|
||||
const sharp = require('sharp')
|
||||
const config = require('../../config')
|
||||
const pkg = require('../../../package.json')
|
||||
const generateKeyPair = promisify(crypto.generateKeyPair)
|
||||
const log = require('../../log')
|
||||
const locales = require('../../../locales/index')
|
||||
|
||||
|
||||
let defaultHostname
|
||||
try {
|
||||
defaultHostname = new URL.URL(config.baseurl).hostname
|
||||
} catch (e) {}
|
||||
|
||||
const defaultSettings = {
|
||||
title: config.title || 'Gancio',
|
||||
description: config.description || 'A shared agenda for local communities',
|
||||
baseurl: config.baseurl || '',
|
||||
hostname: defaultHostname,
|
||||
instance_timezone: 'Europe/Rome',
|
||||
instance_locale: 'en',
|
||||
instance_name: config.title.toLowerCase().replace(/ /g, ''),
|
||||
instance_name: 'gancio',
|
||||
instance_place: '',
|
||||
allow_registration: true,
|
||||
allow_anon_event: true,
|
||||
|
@ -32,7 +39,9 @@ const defaultSettings = {
|
|||
footerLinks: [
|
||||
{ href: '/', label: 'home' },
|
||||
{ href: '/about', label: 'about' }
|
||||
]
|
||||
],
|
||||
admin_email: config.admin_email || '',
|
||||
smtp: config.smtp || false
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -45,54 +54,88 @@ const settingsController = {
|
|||
secretSettings: {},
|
||||
|
||||
async load () {
|
||||
if (!settingsController.settings.initialized) {
|
||||
// initialize instance settings from db
|
||||
// note that this is done only once when the server starts
|
||||
// and not for each request (it's a kind of cache)!
|
||||
const settings = await Setting.findAll()
|
||||
settingsController.settings.initialized = true
|
||||
if (config.firstrun) {
|
||||
settingsController.settings = defaultSettings
|
||||
settings.forEach(s => {
|
||||
if (s.is_secret) {
|
||||
settingsController.secretSettings[s.key] = s.value
|
||||
} else {
|
||||
settingsController.settings[s.key] = s.value
|
||||
return
|
||||
}
|
||||
if (settingsController.settings.initialized) return
|
||||
settingsController.settings.initialized = true
|
||||
// initialize instance settings from db
|
||||
// note that this is done only once when the server starts
|
||||
// and not for each request
|
||||
const Setting = require('../models/setting')
|
||||
const settings = await Setting.findAll()
|
||||
settingsController.settings = defaultSettings
|
||||
settings.forEach(s => {
|
||||
if (s.is_secret) {
|
||||
settingsController.secretSettings[s.key] = s.value
|
||||
} else {
|
||||
settingsController.settings[s.key] = s.value
|
||||
}
|
||||
})
|
||||
|
||||
// add pub/priv instance key if needed
|
||||
if (!settingsController.settings.publicKey) {
|
||||
log.info('Instance priv/pub key not found, generating....')
|
||||
const { publicKey, privateKey } = await generateKeyPair('rsa', {
|
||||
modulusLength: 4096,
|
||||
publicKeyEncoding: {
|
||||
type: 'spki',
|
||||
format: 'pem'
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8',
|
||||
format: 'pem'
|
||||
}
|
||||
})
|
||||
|
||||
// add pub/priv instance key if needed
|
||||
if (!settingsController.settings.publicKey) {
|
||||
log.info('Instance priv/pub key not found, generating....')
|
||||
const { publicKey, privateKey } = await generateKeyPair('rsa', {
|
||||
modulusLength: 4096,
|
||||
publicKeyEncoding: {
|
||||
type: 'spki',
|
||||
format: 'pem'
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8',
|
||||
format: 'pem'
|
||||
await settingsController.set('publicKey', publicKey)
|
||||
await settingsController.set('privateKey', privateKey, true)
|
||||
}
|
||||
|
||||
// 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}]`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// load custom plugins
|
||||
const plugins_path = path.resolve(process.env.cwd || '', 'plugins')
|
||||
if (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)
|
||||
}
|
||||
})
|
||||
|
||||
await settingsController.set('publicKey', publicKey)
|
||||
await settingsController.set('privateKey', privateKey, true)
|
||||
}
|
||||
|
||||
// initialize user_locale
|
||||
if (config.user_locale && fs.existsSync(path.resolve(config.user_locale))) {
|
||||
const user_locale = fs.readdirSync(path.resolve(config.user_locale))
|
||||
user_locale.forEach(async f => {
|
||||
consola.info(`Loading user locale ${f}`)
|
||||
const locale = path.basename(f, '.js')
|
||||
settingsController.user_locale[locale] =
|
||||
(await require(path.resolve(config.user_locale, f))).default
|
||||
})
|
||||
}
|
||||
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)}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async set (key, value, is_secret = false) {
|
||||
const Setting = require('../models/setting')
|
||||
log.info(`SET ${key} ${is_secret ? '*****' : value}`)
|
||||
try {
|
||||
const [setting, created] = await Setting.findOrCreate({
|
||||
|
@ -103,7 +146,7 @@ const settingsController = {
|
|||
settingsController[is_secret ? 'secretSettings' : 'settings'][key] = value
|
||||
return true
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
log.error('[SETTING SET]', e)
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
@ -114,6 +157,19 @@ const settingsController = {
|
|||
if (ret) { res.sendStatus(200) } else { res.sendStatus(400) }
|
||||
},
|
||||
|
||||
async testSMTP (req, res) {
|
||||
const smtp = req.body
|
||||
await settingsController.set('smtp', smtp.smtp)
|
||||
const mail = require('../mail')
|
||||
try {
|
||||
await mail._send(settingsController.settings.admin_email, 'test', null, 'en')
|
||||
return res.sendStatus(200)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return res.status(400).send(String(e))
|
||||
}
|
||||
},
|
||||
|
||||
setLogo (req, res) {
|
||||
if (!req.file) {
|
||||
settingsController.set('logo', false)
|
||||
|
@ -124,16 +180,13 @@ const settingsController = {
|
|||
const baseImgPath = path.resolve(config.upload_path, 'logo')
|
||||
|
||||
// convert and resize to png
|
||||
sharp(uploadedPath)
|
||||
return sharp(uploadedPath)
|
||||
.resize(400)
|
||||
.png({ quality: 90 })
|
||||
.toFile(baseImgPath + '.png', async (err, info) => {
|
||||
.toFile(baseImgPath + '.png', (err, info) => {
|
||||
if (err) {
|
||||
log.error(err)
|
||||
log.error('[LOGO] ' + err)
|
||||
}
|
||||
const image = await readFile(baseImgPath + '.png')
|
||||
const favicon = await toIco([image], { sizes: [64], resize: true })
|
||||
writeFile(baseImgPath + '.ico', favicon)
|
||||
settingsController.set('logo', baseImgPath)
|
||||
res.sendStatus(200)
|
||||
})
|
||||
|
@ -141,14 +194,7 @@ const settingsController = {
|
|||
|
||||
getAllRequest (req, res) {
|
||||
// get public settings and public configuration
|
||||
const settings = {
|
||||
...settingsController.settings,
|
||||
baseurl: config.baseurl,
|
||||
title: config.title,
|
||||
description: config.description,
|
||||
version: pkg.version
|
||||
}
|
||||
res.json(settings)
|
||||
res.json({ ...settingsController.settings, version: pkg.version })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
86
server/api/controller/setup.js
Normal file
86
server/api/controller/setup.js
Normal file
|
@ -0,0 +1,86 @@
|
|||
const URL = require('url')
|
||||
const helpers = require('../../helpers.js')
|
||||
const log = require('../../log')
|
||||
const db = require('../models/index.js')
|
||||
const config = require('../../config')
|
||||
const settingsController = require('./settings')
|
||||
const path = require('path')
|
||||
|
||||
const setupController = {
|
||||
|
||||
async setupDb (req, res, next) {
|
||||
log.debug('[SETUP] Check db')
|
||||
const dbConf = req.body.db
|
||||
if (!dbConf) {
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
|
||||
if (dbConf.storage) {
|
||||
dbConf.storage = path.resolve(process.env.cwd || '', dbConf.storage)
|
||||
}
|
||||
|
||||
try {
|
||||
// try to connect
|
||||
dbConf.logging = false
|
||||
await db.connect(dbConf)
|
||||
|
||||
// is empty ?
|
||||
const isEmpty = await db.isEmpty()
|
||||
if (!isEmpty) {
|
||||
log.warn(' ⚠ Non empty db! Please move your current db elsewhere than retry.')
|
||||
return res.status(400).send(' ⚠ Non empty db! Please move your current db elsewhere than retry.')
|
||||
}
|
||||
|
||||
await db.runMigrations()
|
||||
|
||||
config.db = dbConf
|
||||
config.firstrun = false
|
||||
config.db.logging = false
|
||||
config.baseurl = req.protocol + '://' + req.headers.host
|
||||
config.hostname = new URL.URL(config.baseurl).hostname
|
||||
|
||||
const settingsController = require('./settings')
|
||||
await settingsController.load()
|
||||
return res.sendStatus(200)
|
||||
} catch (e) {
|
||||
return res.status(400).send(String(e))
|
||||
}
|
||||
},
|
||||
|
||||
async restart (req, res) {
|
||||
|
||||
try {
|
||||
|
||||
// write configuration
|
||||
config.write()
|
||||
|
||||
// calculate default settings values
|
||||
await settingsController.set('theme.is_dark', true)
|
||||
await settingsController.set('instance_name', settingsController.settings.title.toLowerCase().replace(/ /g, ''))
|
||||
// create admin
|
||||
const password = helpers.randomString()
|
||||
const email = `admin`
|
||||
const User = require('../models/user')
|
||||
await User.create({
|
||||
email,
|
||||
password,
|
||||
is_admin: true,
|
||||
is_active: true
|
||||
})
|
||||
|
||||
res.json({ password, email })
|
||||
log.info('Restart needed')
|
||||
|
||||
res.end()
|
||||
// exit process so pm2 || docker could restart me || service
|
||||
process.kill(process.pid)
|
||||
|
||||
} catch (e) {
|
||||
log.error(String(e))
|
||||
return res.status(400).send(String(e))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = setupController
|
|
@ -1,6 +1,6 @@
|
|||
const crypto = require('crypto')
|
||||
const { Op } = require('sequelize')
|
||||
const config = require('config')
|
||||
const config = require('../../config')
|
||||
const mail = require('../mail')
|
||||
const User = require('../models/user')
|
||||
const settingsController = require('./settings')
|
||||
|
@ -26,7 +26,7 @@ const userController = {
|
|||
if (!recover_code) { return res.sendStatus(400) }
|
||||
const user = await User.findOne({ where: { recover_code: { [Op.eq]: recover_code } } })
|
||||
if (!user) { return res.sendStatus(400) }
|
||||
res.sendStatus(200)
|
||||
res.json({ email: user.email })
|
||||
},
|
||||
|
||||
async updatePasswordWithRecoverCode (req, res) {
|
||||
|
@ -50,7 +50,7 @@ const userController = {
|
|||
},
|
||||
|
||||
async getAll (req, res) {
|
||||
const users = await User.scope('withoutPassword').findAll({
|
||||
const users = await User.scope(req.user.is_admin ? 'withRecover' : 'withoutPassword').findAll({
|
||||
order: [['is_admin', 'DESC'], ['createdAt', 'DESC']]
|
||||
})
|
||||
res.json(users)
|
||||
|
@ -100,10 +100,10 @@ const userController = {
|
|||
const user = await User.create(req.body)
|
||||
log.info(`Sending registration email to ${user.email}`)
|
||||
mail.send(user.email, 'register', { user, config }, req.settings.locale)
|
||||
mail.send(config.admin_email, 'admin_register', { user, config })
|
||||
mail.send(settingsController.settings.admin_email, 'admin_register', { user, config })
|
||||
res.sendStatus(200)
|
||||
} catch (e) {
|
||||
log.error('Registration error: "%s"', e)
|
||||
log.error('Registration error:', e)
|
||||
res.status(404).json(e)
|
||||
}
|
||||
},
|
||||
|
@ -112,11 +112,11 @@ const userController = {
|
|||
try {
|
||||
req.body.is_active = true
|
||||
req.body.recover_code = crypto.randomBytes(16).toString('hex')
|
||||
const user = await User.create(req.body)
|
||||
const user = await User.scope('withRecover').create(req.body)
|
||||
mail.send(user.email, 'user_confirm', { user, config }, req.settings.locale)
|
||||
res.json(user)
|
||||
} catch (e) {
|
||||
log.error('User creation error: %s', e)
|
||||
log.error('User creation error:', e)
|
||||
res.status(404).json(e)
|
||||
}
|
||||
},
|
||||
|
@ -124,10 +124,11 @@ const userController = {
|
|||
async remove (req, res) {
|
||||
try {
|
||||
const user = await User.findByPk(req.params.id)
|
||||
user.destroy()
|
||||
await user.destroy()
|
||||
log.warn(`User ${user.email} removed!`)
|
||||
res.sendStatus(200)
|
||||
} catch (e) {
|
||||
log.error('User removal error: "%s"', e)
|
||||
log.error('User removal error:"', e)
|
||||
res.status(404).json(e)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,150 +2,166 @@ const express = require('express')
|
|||
const multer = require('multer')
|
||||
const cors = require('cors')()
|
||||
|
||||
const { isAuth, isAdmin } = require('./auth')
|
||||
const eventController = require('./controller/event')
|
||||
const exportController = require('./controller/export')
|
||||
const userController = require('./controller/user')
|
||||
const settingsController = require('./controller/settings')
|
||||
const instanceController = require('./controller/instance')
|
||||
const apUserController = require('./controller/ap_user')
|
||||
const resourceController = require('./controller/resource')
|
||||
const oauthController = require('./controller/oauth')
|
||||
const announceController = require('./controller/announce')
|
||||
const helpers = require('../helpers')
|
||||
const storage = require('./storage')
|
||||
const upload = multer({ storage })
|
||||
|
||||
const config = require('config')
|
||||
const config = require('../config')
|
||||
const log = require('../log')
|
||||
|
||||
const api = express.Router()
|
||||
api.use(express.urlencoded({ extended: false }))
|
||||
api.use(express.json())
|
||||
|
||||
/**
|
||||
* Get current authenticated user
|
||||
* @category User
|
||||
* @name /api/user
|
||||
* @type GET
|
||||
* @example **Response**
|
||||
* ```json
|
||||
{
|
||||
"description" : null,
|
||||
"recover_code" : "",
|
||||
"id" : 1,
|
||||
"createdAt" : "2020-01-29T18:10:16.630Z",
|
||||
"updatedAt" : "2020-01-30T22:42:14.789Z",
|
||||
"is_active" : true,
|
||||
"settings" : "{}",
|
||||
"email" : "eventi@cisti.org",
|
||||
"is_admin" : true
|
||||
|
||||
if (config.firstrun) {
|
||||
|
||||
const setupController = require('./controller/setup')
|
||||
const settingsController = require('./controller/settings')
|
||||
api.post('/settings', settingsController.setRequest)
|
||||
api.post('/setup/db', setupController.setupDb)
|
||||
api.post('/setup/restart', setupController.restart)
|
||||
api.post('/settings/smtp', settingsController.testSMTP)
|
||||
|
||||
} else {
|
||||
|
||||
const { isAuth, isAdmin } = require('./auth')
|
||||
const eventController = require('./controller/event')
|
||||
const settingsController = require('./controller/settings')
|
||||
const exportController = require('./controller/export')
|
||||
const userController = require('./controller/user')
|
||||
const instanceController = require('./controller/instance')
|
||||
const apUserController = require('./controller/ap_user')
|
||||
const resourceController = require('./controller/resource')
|
||||
const oauthController = require('./controller/oauth')
|
||||
const announceController = require('./controller/announce')
|
||||
const helpers = require('../helpers')
|
||||
const storage = require('./storage')
|
||||
const upload = multer({ storage })
|
||||
|
||||
/**
|
||||
* Get current authenticated user
|
||||
* @category User
|
||||
* @name /api/user
|
||||
* @type GET
|
||||
* @example **Response**
|
||||
* ```json
|
||||
{
|
||||
"description" : null,
|
||||
"recover_code" : "",
|
||||
"id" : 1,
|
||||
"createdAt" : "2020-01-29T18:10:16.630Z",
|
||||
"updatedAt" : "2020-01-30T22:42:14.789Z",
|
||||
"is_active" : true,
|
||||
"settings" : "{}",
|
||||
"email" : "eventi@cisti.org",
|
||||
"is_admin" : true
|
||||
}
|
||||
```
|
||||
*/
|
||||
api.get('/ping', (req, res) => res.sendStatus(200))
|
||||
api.get('/user', isAuth, (req, res) => res.json(req.user))
|
||||
|
||||
|
||||
api.post('/user/recover', userController.forgotPassword)
|
||||
api.post('/user/check_recover_code', userController.checkRecoverCode)
|
||||
api.post('/user/recover_password', userController.updatePasswordWithRecoverCode)
|
||||
|
||||
// register and add users
|
||||
api.post('/user/register', userController.register)
|
||||
api.post('/user', isAdmin, userController.create)
|
||||
|
||||
// update user
|
||||
api.put('/user', isAuth, userController.update)
|
||||
|
||||
// delete user
|
||||
api.delete('/user/:id', isAdmin, userController.remove)
|
||||
api.delete('/user', isAdmin, userController.remove)
|
||||
|
||||
// get all users
|
||||
api.get('/users', isAdmin, userController.getAll)
|
||||
|
||||
// update a place (modify address..)
|
||||
api.put('/place', isAdmin, eventController.updatePlace)
|
||||
|
||||
/**
|
||||
* Add a new event
|
||||
* @category Event
|
||||
* @name /event
|
||||
* @type POST
|
||||
* @info `Content-Type` has to be `multipart/form-data` to support image upload
|
||||
* @param {string} title - event's title
|
||||
* @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 {integer} start_datetime - start timestamp
|
||||
* @param {integer} multidate - is a multidate event?
|
||||
* @param {array} tags - List of tags
|
||||
* @param {object} [recurrent] - Recurrent event details
|
||||
* @param {string} [recurrent.frequency] - could be `1w` or `2w`
|
||||
* @param {string} [recurrent.type] - not used
|
||||
* @param {array} [recurrent.days] - array of days
|
||||
* @param {image} [image] - Image
|
||||
*/
|
||||
|
||||
// allow anyone to add an event (anon event has to be confirmed, TODO: flood protection)
|
||||
api.post('/event', upload.single('image'), eventController.add)
|
||||
|
||||
api.put('/event', isAuth, upload.single('image'), eventController.update)
|
||||
api.get('/event/import', isAuth, helpers.importURL)
|
||||
|
||||
// remove event
|
||||
api.delete('/event/:id', isAuth, eventController.remove)
|
||||
|
||||
// get tags/places
|
||||
api.get('/event/meta', eventController.getMeta)
|
||||
|
||||
// get unconfirmed events
|
||||
api.get('/event/unconfirmed', isAdmin, eventController.getUnconfirmed)
|
||||
|
||||
// add event notification TODO
|
||||
api.post('/event/notification', eventController.addNotification)
|
||||
api.delete('/event/notification/:code', eventController.delNotification)
|
||||
|
||||
api.get('/settings', settingsController.getAllRequest)
|
||||
api.post('/settings', isAdmin, settingsController.setRequest)
|
||||
api.post('/settings/logo', isAdmin, multer({ dest: config.upload_path }).single('logo'), settingsController.setLogo)
|
||||
api.post('/settings/smtp', isAdmin, settingsController.testSMTP)
|
||||
|
||||
// confirm event
|
||||
api.put('/event/confirm/:event_id', isAuth, eventController.confirm)
|
||||
api.put('/event/unconfirm/:event_id', isAuth, eventController.unconfirm)
|
||||
|
||||
// get event
|
||||
api.get('/event/:event_slug.:format?', cors, eventController.get)
|
||||
|
||||
// export events (rss/ics)
|
||||
api.get('/export/:type', cors, exportController.export)
|
||||
|
||||
// get events in this range
|
||||
api.get('/events', cors, eventController.select)
|
||||
|
||||
api.get('/instances', isAdmin, instanceController.getAll)
|
||||
api.get('/instances/:instance_domain', isAdmin, instanceController.get)
|
||||
api.post('/instances/toggle_block', isAdmin, instanceController.toggleBlock)
|
||||
api.post('/instances/toggle_user_block', isAdmin, apUserController.toggleBlock)
|
||||
api.put('/resources/:resource_id', isAdmin, resourceController.hide)
|
||||
api.delete('/resources/:resource_id', isAdmin, resourceController.remove)
|
||||
api.get('/resources', isAdmin, resourceController.getAll)
|
||||
|
||||
// - ADMIN ANNOUNCEMENTS
|
||||
api.get('/announcements', isAdmin, announceController.getAll)
|
||||
api.post('/announcements', isAdmin, announceController.add)
|
||||
api.put('/announcements/:announce_id', isAdmin, announceController.update)
|
||||
api.delete('/announcements/:announce_id', isAdmin, announceController.remove)
|
||||
|
||||
// OAUTH
|
||||
api.get('/clients', isAuth, oauthController.getClients)
|
||||
api.get('/client/:client_id', isAuth, oauthController.getClient)
|
||||
api.post('/client', oauthController.createClient)
|
||||
}
|
||||
```
|
||||
*/
|
||||
api.get('/user', isAuth, (req, res) => res.json(req.user))
|
||||
|
||||
api.post('/user/recover', userController.forgotPassword)
|
||||
api.post('/user/check_recover_code', userController.checkRecoverCode)
|
||||
api.post('/user/recover_password', userController.updatePasswordWithRecoverCode)
|
||||
|
||||
// register and add users
|
||||
api.post('/user/register', userController.register)
|
||||
api.post('/user', isAdmin, userController.create)
|
||||
|
||||
// update user
|
||||
api.put('/user', isAuth, userController.update)
|
||||
|
||||
// delete user
|
||||
api.delete('/user/:id', isAdmin, userController.remove)
|
||||
api.delete('/user', isAdmin, userController.remove)
|
||||
|
||||
// get all users
|
||||
api.get('/users', isAdmin, userController.getAll)
|
||||
|
||||
// update a place (modify address..)
|
||||
api.put('/place', isAdmin, eventController.updatePlace)
|
||||
|
||||
/**
|
||||
* Add a new event
|
||||
* @category Event
|
||||
* @name /event
|
||||
* @type POST
|
||||
* @info `Content-Type` has to be `multipart/form-data` to support image upload
|
||||
* @param {string} title - event's title
|
||||
* @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 {integer} start_datetime - start timestamp
|
||||
* @param {integer} multidate - is a multidate event?
|
||||
* @param {array} tags - List of tags
|
||||
* @param {object} [recurrent] - Recurrent event details
|
||||
* @param {string} [recurrent.frequency] - could be `1w` or `2w`
|
||||
* @param {string} [recurrent.type] - not used
|
||||
* @param {array} [recurrent.days] - array of days
|
||||
* @param {image} [image] - Image
|
||||
*/
|
||||
|
||||
// allow anyone to add an event (anon event has to be confirmed, TODO: flood protection)
|
||||
api.post('/event', upload.single('image'), eventController.add)
|
||||
|
||||
api.put('/event', isAuth, upload.single('image'), eventController.update)
|
||||
api.get('/event/import', isAuth, helpers.importURL)
|
||||
|
||||
// remove event
|
||||
api.delete('/event/:id', isAuth, eventController.remove)
|
||||
|
||||
// get tags/places
|
||||
api.get('/event/meta', eventController.getMeta)
|
||||
|
||||
// get unconfirmed events
|
||||
api.get('/event/unconfirmed', isAdmin, eventController.getUnconfirmed)
|
||||
|
||||
// add event notification TODO
|
||||
api.post('/event/notification', eventController.addNotification)
|
||||
api.delete('/event/notification/:code', eventController.delNotification)
|
||||
|
||||
api.get('/settings', settingsController.getAllRequest)
|
||||
api.post('/settings', isAdmin, settingsController.setRequest)
|
||||
api.post('/settings/logo', isAdmin, multer({ dest: config.upload_path }).single('logo'), settingsController.setLogo)
|
||||
|
||||
// confirm event
|
||||
api.put('/event/confirm/:event_id', isAuth, eventController.confirm)
|
||||
api.put('/event/unconfirm/:event_id', isAuth, eventController.unconfirm)
|
||||
|
||||
// get event
|
||||
api.get('/event/:event_id.:format?', cors, eventController.get)
|
||||
|
||||
// export events (rss/ics)
|
||||
api.get('/export/:type', cors, exportController.export)
|
||||
|
||||
// get events in this range
|
||||
api.get('/events', cors, eventController.select)
|
||||
|
||||
api.get('/instances', isAdmin, instanceController.getAll)
|
||||
api.get('/instances/:instance_domain', isAdmin, instanceController.get)
|
||||
api.post('/instances/toggle_block', isAdmin, instanceController.toggleBlock)
|
||||
api.post('/instances/toggle_user_block', isAdmin, apUserController.toggleBlock)
|
||||
api.put('/resources/:resource_id', isAdmin, resourceController.hide)
|
||||
api.delete('/resources/:resource_id', isAdmin, resourceController.remove)
|
||||
api.get('/resources', isAdmin, resourceController.getAll)
|
||||
|
||||
// - ADMIN ANNOUNCEMENTS
|
||||
api.get('/announcements', isAdmin, announceController.getAll)
|
||||
api.post('/announcements', isAdmin, announceController.add)
|
||||
api.put('/announcements/:announce_id', isAdmin, announceController.update)
|
||||
api.delete('/announcements/:announce_id', isAdmin, announceController.remove)
|
||||
|
||||
// OAUTH
|
||||
api.get('/clients', isAuth, oauthController.getClients)
|
||||
api.get('/client/:client_id', isAuth, oauthController.getClient)
|
||||
api.post('/client', oauthController.createClient)
|
||||
|
||||
api.use((req, res) => res.sendStatus(404))
|
||||
|
||||
// Handle 500
|
||||
api.use((error, req, res, next) => {
|
||||
log.error(error)
|
||||
log.error('[API ERROR]', error)
|
||||
res.status(500).send('500: Internal Server Error')
|
||||
})
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
const Email = require('email-templates')
|
||||
const path = require('path')
|
||||
const moment = require('dayjs')
|
||||
const config = require('config')
|
||||
const settingsController = require('./controller/settings')
|
||||
const log = require('../log')
|
||||
const { Task, TaskManager } = require('../taskManager')
|
||||
|
@ -9,7 +8,11 @@ const locales = require('../../locales')
|
|||
|
||||
const mail = {
|
||||
send (addresses, template, locals, locale = settingsController.settings.instance_locale) {
|
||||
log.debug('Enqueue new email ', template, locale)
|
||||
if (process.env.NODE_ENV === 'production' && (!settingsController.settings.admin_email || !settingsController.settings.smtp)) {
|
||||
log.error(`Cannot send any email: SMTP Email configuration not completed!`)
|
||||
return
|
||||
}
|
||||
log.debug(`Enqueue new email ${template} ${locale}`)
|
||||
const task = new Task({
|
||||
name: 'MAIL',
|
||||
method: mail._send,
|
||||
|
@ -18,7 +21,8 @@ const mail = {
|
|||
TaskManager.add(task)
|
||||
},
|
||||
|
||||
_send (addresses, template, locals, locale) {
|
||||
_send (addresses, template, locals, locale = settingsController.settings.instance_locale) {
|
||||
const settings = settingsController.settings
|
||||
log.info(`Send ${template} email to ${addresses} with locale ${locale}`)
|
||||
const email = new Email({
|
||||
views: { root: path.join(__dirname, '..', 'emails') },
|
||||
|
@ -31,7 +35,7 @@ const mail = {
|
|||
}
|
||||
},
|
||||
message: {
|
||||
from: `📅 ${config.title} <${config.admin_email}>`
|
||||
from: `📅 ${settings.title} <${settings.admin_email}>`
|
||||
},
|
||||
send: true,
|
||||
i18n: {
|
||||
|
@ -39,29 +43,29 @@ const mail = {
|
|||
objectNotation: true,
|
||||
syncFiles: false,
|
||||
updateFiles: false,
|
||||
defaultLocale: settingsController.settings.instance_locale || 'en',
|
||||
defaultLocale: settings.instance_locale || 'en',
|
||||
locale,
|
||||
locales: Object.keys(locales)
|
||||
},
|
||||
transport: config.smtp
|
||||
transport: settings.smtp || {}
|
||||
})
|
||||
|
||||
const msg = {
|
||||
template,
|
||||
message: {
|
||||
to: addresses,
|
||||
bcc: config.admin_email
|
||||
to: addresses
|
||||
},
|
||||
locals: {
|
||||
...locals,
|
||||
locale,
|
||||
config: { title: config.title, baseurl: config.baseurl, description: config.description, admin_email: config.admin_email },
|
||||
config: { title: settings.title, baseurl: settings.baseurl, description: settings.description, admin_email: settings.admin_email },
|
||||
datetime: datetime => moment.unix(datetime).locale(locale).format('ddd, D MMMM HH:mm')
|
||||
}
|
||||
}
|
||||
return email.send(msg)
|
||||
.catch(e => {
|
||||
log.error('Error sending email => %s', e)
|
||||
log.error('[MAIL]', e)
|
||||
throw e
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const sequelize = require('./index')
|
||||
const sequelize = require('./index').sequelize
|
||||
const { Model, DataTypes } = require('sequelize')
|
||||
|
||||
class Announcement extends Model {}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue