Merge branch 'master' into gh

This commit is contained in:
lesion 2022-03-11 23:22:12 +01:00
commit 5dddfbd29e
No known key found for this signature in database
GPG key ID: 352918250B012177
126 changed files with 6962 additions and 2739 deletions

9
.gitignore vendored
View file

@ -1,13 +1,14 @@
# Created by .ignore support plugin (hsz.mobi)
### Gancio dev configuration
*.sqlite
gancio.sqlite
db.sqlite
releases
wp-plugin/wpgancio
config/development.json
gancio_config.json
config.json
db.sqlite
/gancio_config.json
/config.json
/assets/config.json
thumb
docs/_site
.vscode

View file

@ -1,11 +1,77 @@
All notable changes to this project will be documented in this file.
### UNRELEASED
- add CLI support to manage accounts (list / modify / add accounts)
### 1.4.3 - 10 mar '22
- fix [#140](https://framagit.org/les/gancio/-/issues/140) - Invalid date
- fix [#141](https://framagit.org/les/gancio/-/issues/141) - Cannot change logo
- fix same day events
- add missing icons in admin
- prepare multisite settings
- improve initialization
- start unit testing API (it's never too late)
### 1.4.1 - 4 mar '22
- add gl/galego locale, thanks @xosem
- fix import redirect loop
- add missing icons (close, repeat, arrows ...)
- turn rss icon into a real link to improve a11y
- force seconds to 0 for each events, fix recurring events starting date issue
- fix next/prev selection on same datetime events
- improve moderation UI (add author and event link + format creation date)
- refactoring resource UI from fedi
### 1.4.0 - 9 feb '22
- improve Cumulative Layout Shift
- remove filename as default media label to avoid leak metadata
- add endData to microdata
- security fix with filtering settings, avoid sharing SMTP pass with front-end
- fix broken SMTP
- remove global materialicons / vuetify css, use threeshake and @nuxt/vuetify (really improve lighthouse score)
- new Dockerfile using node:17.4-slim as base img (from 1.5Gb to ~800Mb)
- add XSS and path traversal mitigation
- improve a11y
- update deps
### 1.3.3 - 1 feb '22
- security fix, avoid sharing smtp pass with front-end
### 1.3.2 - 1 feb '22
- fix webcomponent for event without img
### 1.3.1 - 1 feb '22
- inherits tags in recurring events [#138](https://framagit.org/les/gancio/-/issues/138)
- you can now skip an occurrence of a recurring event
- fix `show_recurrent` event in webcomponent and API
- add new webcomponent `sidebar` attribute and a [`fullwith` layout](https://gancio.org/usage/embed#embed-event-lists)
### 1.3.0 - 26 gen '22
- add mariadb support
- add [microdata](https://developer.mozilla.org/en-US/docs/Web/HTML/Microdata) support
- support db setup via environment variables (used in updated `docker-compose.yml` files)
- improve rss feed:
- fix validation
- add enclusure media for featured image
- add categories
- fix typo in export code
- add theme attribute to gancio-events webcomponent (dark/light)
- increase thumbs quality
- improve logo for light theme
- improve [wordpress plugin](https://wordpress.org/plugins/wpgancio/)
- add \[gancio-event\] and \[gancio-events\] shortcode
- allow gancio-events / gancio-event tags in editor
- automatically enqueue webcomponent script
- tags/places filters are now inclusive not exclusive
- fix image undefined alternative text
- update documentation, dependencies, translations
### 1.2.2 - 7 dic '21
- shiny new gancio-event\[s\] webcomponents => [docs](https://gancio.org/usage/embed)
- new backend plugin system
- new backend plugin system => [docs](https://gancio.org/dev/plugins)
- improve media focal point selection
- improve non-js experience (load img, use native lazy loading)
- improve user_confirm / recover code flow
- permit admins to choose user password (usefull on instance without SMTP configuration)
- 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)

View file

@ -1,11 +1,7 @@
- change version in package.json
- add changes to CHANGELOG / changelog.md
- yarn build
- yarn pack
- yarn publish
- yarn doc
./release.sh
- git add .
- git ci -m 'v...'
- git tag ...
- git push --tags
-

View file

@ -12,7 +12,7 @@ export default {
document.execCommand('copy')
document.body.removeChild(el)
}
this.$root.$message(msg)
this.$root.$message(msg, { color: 'success'})
}
}
}

View file

@ -1,61 +0,0 @@
.editor {
position: relative;
overflow-y: auto;
padding-top: 1.7em;
scrollbar-width: thin;
&.with-border {
border: 1px solid #ddd;
border-radius: 5px;
}
.content {
padding: 0px 5px 0px 5px;
flex: 1;
scrollbar-width: thin;
overflow-y: auto;
}
.menububble {
position: absolute;
display: flex;
overflow: hidden;
opacity: 0;
z-index: 1;
background: #dddddd;
transform: translateX(-50%);
border-radius: 3px;
padding: 0.07rem;
transition: opacity 0.2s, visibility 0.2s, left .2s, bottom .2s;
visibility: hidden;
&.is-active {
opacity: 1;
visibility: visible;
}
input {
padding: 0;
margin: 1px;
display: block;
border: 0;
color: #444;
font-size: .8em;
border-radius: 3px;
line-height: 100%;
transition: width .2s;
padding-left: 5px;
flex-grow: 1;
}
.fa-icon {
width: auto;
font-size: 10px;
height: 1.4em; /* or any other relative font sizes */
/* You would have to include the following two lines to make this work in Safari */
// max-width: 100%;
max-height: 100%;
}
}
}

View file

@ -1,56 +0,0 @@
// .event {
// width: 320px;
// max-width: 450px;
// flex-grow: 1;
// margin: .2em;
// background-color: #202020;
// overflow: hidden;
// a:hover {
// text-decoration: none;
// .title {
// border-bottom: 1px solid #888;
// color: white;
// }
// }
// .title {
// margin-left: 1rem;
// margin-top: 1rem;
// margin-right: 1rem;
// border-bottom: 1px solid #333;
// transition: border-color .5s;
// font-size: 1.2em;
// max-height: 3em;
// overflow: hidden;
// color: white;
// font-weight: bold;
// }
// .card-footer {
// max-height: 4.5em;
// overflow: hidden;
// padding: .25rem 0.5rem;
// line-height: 1.8rem;
// min-height: 2.2rem;
// }
// .card-body {
// overflow: hidden;
// }
// .description {
// color: #999;
// font-size: 0.8em;
// overflow: hidden;
// max-height: 100%;
// }
// .el-image { width: 100% }
// img {
// width: 100%;
// max-height: 250px;
// object-fit: cover;
// object-position: top;
// }
// }

View file

@ -30,6 +30,10 @@ li {
justify-content: center;
}
#calh {
height: 292px;
}
.container {
max-width: 1400px;
}
@ -91,7 +95,6 @@ li {
.place {
max-width: 100%;
span {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

8
assets/variables.scss Normal file
View file

@ -0,0 +1,8 @@
// assets/variables.scss
// Variables you want to modify
// $btn-border-radius: 0px;
// If you need to extend Vuetify SASS lists
// $material-light: ( cards: blue );
@import '~vuetify/src/styles/styles.sass';

View file

@ -1,15 +1,16 @@
<template lang="pug">
nuxt-link(:to='`/announcement/${announcement.id}`')
v-alert.mb-1(border='left' type='info' color="primary" show-icon) {{announcement.title}}
v-alert.mb-1(border='left' type='info' color="primary" :icon='mdiInformation') {{announcement.title}}
</template>
<script>
import { mapState } from 'vuex'
import { mdiInformation } from '@mdi/js'
export default {
data () {
return { mdiInformation }
},
props: {
announcement: { type: Object, default: () => ({}) }
},
computed: mapState(['announcements'])
}
}
</script>

View file

@ -1,17 +1,18 @@
<template lang="pug">
#calendar
vc-calendar(
vc-date-picker(
v-model='selectedDate'
title-position='left'
:is-dark="settings['theme.is_dark']"
:columns="$screens({ sm: 2 }, 1)"
@input='click'
@update:from-page='updatePage'
:locale='$i18n.locale'
:attributes='attributes'
transition='fade'
aria-label='Calendar'
is-expanded
is-inline
@dayclick='click')
is-inline)
</template>
<script>
@ -28,6 +29,7 @@ export default {
const month = dayjs().month() + 1
const year = dayjs().year()
return {
selectedDate: null,
page: { month, year }
}
},

View file

@ -7,64 +7,67 @@
v-btn(icon text tabindex='-1'
:class="{ primary: isActive.bold() }"
@click="commands.bold")
v-icon mdi-format-bold
v-icon(v-text='mdiFormatBold')
v-btn(icon text tabindex='-1'
:class="{ primary: isActive.underline() }"
@click="commands.underline")
v-icon mdi-format-underline
v-icon(v-text='mdiFormatUnderline')
v-btn(icon text tabindex='-1'
:class="{ primary: isActive.strike() }"
@click="commands.strike")
v-icon mdi-format-strikethrough-variant
v-icon(v-text='mdiFormatStrikethroughVariant')
v-btn(icon text tabindex='-1'
:class="{ primary: isActive.italic() }"
@click="commands.italic")
v-icon mdi-format-italic
v-icon(v-text='mdiFormatItalic')
v-btn(icon text tabindex='-1'
:class="{ primary: isActive.heading({level: 1}) }"
@click="commands.heading({level: 1})")
v-icon mdi-format-header-1
v-icon(v-text='mdiFormatHeader1')
v-btn(icon text tabindex='-1'
:class="{ primary: isActive.heading({level: 2}) }"
@click="commands.heading({level: 2})")
v-icon mdi-format-header-2
v-icon(v-text='mdiFormatHeader2')
v-btn(icon text tabindex='-1'
:class="{ primary: isActive.heading({level: 3}) }"
@click="commands.heading({level: 3})")
v-icon mdi-format-header-3
v-icon(v-text='mdiFormatHeader3')
v-btn(icon text tabindex='-1'
:class="{ primary: isActive.code() }"
@click="commands.code")
v-icon mdi-code-tags
v-icon(v-text='mdiCodeTags')
v-btn(icon text tabindex='-1'
:class="{ primary: isActive.blockquote() }"
@click="commands.blockquote")
v-icon mdi-format-quote-open
v-icon(v-text='mdiFormatQuoteOpen')
v-btn(icon text tabindex='-1'
:class="{ primary: isActive.bullet_list() }"
@click="commands.bullet_list")
v-icon mdi-format-list-bulleted
v-icon(v-text='mdiFormatListBulleted')
v-btn(icon text tabindex='-1' :class='{ primary: isActive.link() }'
@click='commands.link({href: getMarkAttrs("link") && getMarkAttrs("link").href ? "" : "https://"}); $refs.link.focus();')
v-icon mdi-link
v-icon(v-text='mdiLink')
v-text-field.pt-0.ml-1(v-show='isActive.link()' ref='link' @focus='focus' @blur='blur' hide-details
:value='isActive.link() && getMarkAttrs("link") && getMarkAttrs("link").href || ""'
@keypress.enter='commands.link({ href: $event.target.value}); editor.focus()')
editor-content.content(:editor='editor' spellcheck='false' :style="{ 'max-height': maxHeight }")
editor-content.content(:editor='editor' spellcheck='false' :style="{ 'max-height': maxHeight }" :aria-label='label' :label='label')
</template>
<script>
import debounce from 'lodash/debounce'
import { mdiLink, mdiFormatListBulleted, mdiFormatQuoteOpen, mdiCodeTags,
mdiFormatHeader1, mdiFormatHeader2, mdiFormatHeader3, mdiFormatItalic,
mdiFormatStrikethroughVariant, mdiFormatBold, mdiFormatUnderline } from '@mdi/js'
import { Editor, EditorContent, EditorMenuBar, EditorMenuBubble } from 'tiptap'
import {
Blockquote,
@ -97,6 +100,9 @@ export default {
},
data () {
return {
mdiLink, mdiFormatListBulleted, mdiFormatQuoteOpen, mdiCodeTags,
mdiFormatHeader1, mdiFormatHeader2, mdiFormatHeader3, mdiFormatItalic,
mdiFormatStrikethroughVariant, mdiFormatBold, mdiFormatUnderline,
options: [],
linkActive: false,
editor: null,

View file

@ -1,54 +1,66 @@
<template lang="pug">
v-card.h-event.event.d-flex
nuxt-link(:to='`/event/${event.slug || event.id}`')
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}}
v-card.h-event.event.d-flex(itemscope itemtype="https://schema.org/Event")
nuxt-link(:to='`/event/${event.slug || event.id}`' itemprop="url")
img.img.u-featured(:src='thumbnail' :alt='alt' :loading='this.lazy?"lazy":"eager"' itemprop="image" :style="{ 'object-position': thumbnailPosition }")
v-icon.float-right.mr-1(v-if='event.parentId' color='success' v-text='mdiRepeat')
.title.p-name(itemprop="name") {{event.title}}
v-card-text.body.pt-0.pb-0
time.dt-start.subtitle-1(:datetime='event.start_datetime|unixFormat("YYYY-MM-DD HH:mm")') <v-icon>mdi-calendar</v-icon> {{ event|when }}
.d-none.dt-end {{event.end_datetime|unixFormat('YYYY-MM-DD HH:mm')}}
a.place.d-block.p-location.pl-0(text color='primary' @click="$emit('placeclick', event.place.id)") <v-icon>mdi-map-marker</v-icon> {{event.place.name}}
time.dt-start.subtitle-1(:datetime='event.start_datetime|unixFormat("YYYY-MM-DD HH:mm")' itemprop="startDate" :content="event.start_datetime|unixFormat('YYYY-MM-DDTHH:mm')") <v-icon v-text='mdiCalendar'></v-icon> {{ event|when }}
.d-none.dt-end(itemprop="endDate" :content="event.end_datetime|unixFormat('YYYY-MM-DDTHH:mm')") {{event.end_datetime|unixFormat('YYYY-MM-DD HH:mm')}}
a.place.d-block.p-location.pl-0(text color='primary' @click="$emit('placeclick', event.place.id)" itemprop="location" :content="event.place.name") <v-icon v-text='mdiMapMarker'></v-icon> {{event.place.name}}
.d-none(itemprop='location.address') {{event.place.address}}
v-card-actions.pt-0.actions.justify-space-between
.tags
v-chip.ml-1.mt-1(v-for='tag in event.tags.slice(0,6)' small
:key='tag' outlined color='primary' @click="$emit('tagclick', tag)") {{tag}}
client-only
v-menu(offset-y)
template(v-slot:activator="{on}")
v-btn.align-self-end(icon v-on='on' color='primary' alt='more')
v-icon mdi-dots-vertical
v-btn.align-self-end(icon v-on='on' color='primary' title='more' aria-label='more')
v-icon(v-text='mdiDotsVertical')
v-list(dense)
v-list-item-group
v-list-item(@click='clipboard(`${settings.baseurl}/event/${event.slug || event.id}`)')
v-list-item-icon
v-icon mdi-content-copy
v-icon(v-text='mdiContentCopy')
v-list-item-content
v-list-item-title {{$t('common.copy_link')}}
v-list-item(:href='`/api/event/${event.slug || event.id}.ics`')
v-list-item-icon
v-icon mdi-calendar-export
v-icon(v-text='mdiCalendarExport')
v-list-item-content
v-list-item-title {{$t('common.add_to_calendar')}}
v-list-item(v-if='is_mine' :to='`/add/${event.id}`')
v-list-item-icon
v-icon mdi-pencil
v-icon(v-text='mdiPencil')
v-list-item-content
v-list-item-title {{$t('common.edit')}}
v-list-item(v-if='is_mine' @click='remove(false)')
v-list-item-icon
v-icon(color='error') mdi-delete-forever
v-icon(color='error' v-text='mdiDeleteForever')
v-list-item-content
v-list-item-title {{$t('common.remove')}}
template(#placeholder)
v-btn.align-self-end(icon color='primary' aria-label='more')
v-icon(v-text='mdiDotsVertical')
</template>
<script>
import { mapState } from 'vuex'
import clipboard from '../assets/clipboard'
import { mdiRepeat, mdiPencil, mdiDotsVertical, mdiContentCopy,
mdiCalendarExport, mdiDeleteForever, mdiCalendar, mdiMapMarker } from '@mdi/js'
export default {
data () {
return { mdiRepeat, mdiPencil, mdiDotsVertical, mdiContentCopy, mdiCalendarExport,
mdiDeleteForever, mdiMapMarker, mdiCalendar }
},
props: {
event: { type: Object, default: () => ({}) }
event: { type: Object, default: () => ({}) },
lazy: Boolean
},
mixins: [clipboard],
computed: {

View file

@ -4,7 +4,7 @@
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>
v-btn(color='primary' text href='https://gancio.org' target='_blank' rel="noopener") Gancio <small>{{settings.version}}</small>
v-btn.ml-1(v-for='link in footerLinks'
:key='link.label' color='primary' text
:href='link.href' :to='link.to' :target="link.href && '_blank'") {{link.label}}

View file

@ -9,14 +9,18 @@ div#list
v-for='event in computedEvents'
:key='`${event.id}_${event.start_datetime}`' small)
v-list-item-content
v-list-item-subtitle <v-icon small color='success' v-if='event.parentId'>mdi-repeat</v-icon> {{event|when}}
v-list-item-subtitle <v-icon small color='success' v-if='event.parentId' v-text='mdiRepeat'></v-icon> {{event|when}}
span.primary--text.ml-1 @{{event.place.name}}
v-list-item-title(v-text='event.title')
</template>
<script>
import { mdiRepeat } from '@mdi/js'
export default {
name: 'List',
data () {
return { mdiRepeat }
},
props: {
title: {
type: String,

View file

@ -1,71 +1,73 @@
<template lang="pug">
v-app-bar(app aria-label='Menu')
v-app-bar(app aria-label='Menu' height=64)
//- logo, title and description
v-list-item(:to='$route.name==="index"?"/about":"/"')
v-list-item-avatar(tile)
v-img(src='/logo.png')
v-img(src='/logo.png' alt='home')
v-list-item-content.d-none.d-sm-flex
v-list-item-title
h2 {{settings.title}}
v-list-item-subtitle {{settings.description}}
v-spacer
v-btn(v-if='$auth.loggedIn || settings.allow_anon_event' icon nuxt to='/add' :aria-label='$t("common.add_event")' :title='$t("common.add_event")')
v-icon(large color='primary' v-text='mdiPlus')
v-tooltip(bottom) {{$t('common.add_event')}}
template(v-slot:activator='{ 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-btn(icon nuxt to='/export' :title='$t("common.share")' :aria-label='$t("common.share")')
v-icon(v-text='mdiShareVariant')
v-tooltip(bottom) {{$t('common.share')}}
template(v-slot:activator='{ on }')
v-btn(icon nuxt to='/export' v-on='on' :aria-label='$t("common.share")')
v-icon mdi-share-variant
v-btn(v-if='!$auth.loggedIn' icon nuxt to='/login' :title='$t("common.login")' :aria-label='$t("common.login")')
v-icon(v-text='mdiLogin')
v-tooltip(v-if='!$auth.loggedIn' bottom) {{$t('common.login')}}
template(v-slot:activator='{ 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")
client-only
v-menu(v-if='$auth.loggedIn' offset-y)
template(v-slot:activator="{ on, attrs }")
v-btn(icon v-bind='attrs' v-on='on' aria-label='Menu')
v-icon mdi-dots-vertical
v-btn(icon v-bind='attrs' v-on='on' title='Menu' aria-label='Menu')
v-icon(v-text='mdiDotsVertical')
v-list
v-list-item(nuxt to='/settings')
v-list-item-icon
v-icon mdi-cog
v-icon(v-text='mdiCog')
v-list-item-content
v-list-item-title {{$t('common.settings')}}
v-list-item(v-if='$auth.user.is_admin' nuxt to='/admin')
v-list-item-icon
v-icon mdi-account
v-icon(v-text='mdiAccount')
v-list-item-content
v-list-item-title {{$t('common.admin')}}
v-list-item(@click='logout')
v-list-item-icon
v-icon mdi-logout
v-icon(v-text='mdiLogout')
v-list-item-content
v-list-item-title {{$t('common.logout')}}
template(#placeholder)
v-btn(v-if='$auth.loggedIn' icon aria-label='Menu' title='Menu')
v-icon(v-text='mdiDotsVertical')
v-btn(icon @click='clipboard(feedLink, "common.feed_url_copied")' aria-label='RSS')
v-icon(color='orange') mdi-rss
v-btn(icon target='_blank' :href='feedLink' title='RSS' aria-label='RSS')
v-icon(color='orange' v-text='mdiRss')
</template>
<script>
import { mapState } from 'vuex'
import clipboard from '../assets/clipboard'
import { mdiPlus, mdiShareVariant, mdiLogin, mdiDotsVertical, mdiLogout, mdiAccount, mdiCog, mdiRss } from '@mdi/js'
export default {
name: 'Nav',
data () {
return { mdiPlus, mdiShareVariant, mdiLogout, mdiLogin, mdiDotsVertical, mdiAccount, mdiCog, mdiRss }
},
mixins: [clipboard],
computed: {
...mapState(['filters', 'settings']),
feedLink () {
const tags = this.filters.tags && this.filters.tags.join(',')
const tags = this.filters.tags && this.filters.tags.map(encodeURIComponent).join(',')
const places = this.filters.places && this.filters.places.join(',')
let query = ''
if (tags || places) {
@ -80,32 +82,11 @@ export default {
return `${this.settings.baseurl}/feed/rss${query}`
},
could_add () {
return (this.$auth.loggedIn || this.settings.allow_anon_event)
}
},
methods: {
logout () {
this.$root.$message('common.logout_ok')
this.$auth.logout()
},
async createTrustedInstance () {
let url = this.instance_url
if (!url.match(/^https?:\/\//)) {
url = `https://${url}`
}
try {
const instance = await this.$axios.$get(`${url}/.well-known/nodeinfo/2.0`)
const trusted_instance = {
url,
name: instance.metadata.nodeName,
description: instance.metadata.nodeDescription,
place: instance.metadata.placeDescription
}
this.setSetting({ key: 'trusted_instances', value: this.settings.trusted_instances.concat(trusted_instance) })
} catch (e) {
this.$root.$message(e, { color: 'error' })
}
}
}
}

View file

@ -21,20 +21,22 @@
template(v-slot:selection="data")
v-chip(v-bind="data.attrs"
close
:close-icon='mdiCloseCircle'
@click:close='remove(data.item)'
:input-value="data.selected")
v-avatar(left)
v-icon {{data.item.type === 'place' ? 'mdi-map-marker' : 'mdi-tag' }}
v-icon(v-text="data.item.type === 'place' ? mdiMapMarker : mdiTag")
span {{ data.item.label }}
template(v-slot:item='{ item }')
v-list-item-avatar
v-icon {{item.type === 'place' ? 'mdi-map-marker' : 'mdi-tag' }}
v-icon(v-text="item.type === 'place' ? mdiMapMarker : mdiTag")
v-list-item-content
v-list-item-title(v-text='item.label')
</template>
<script>
import { mapState } from 'vuex'
import { mdiMapMarker, mdiTag, mdiCloseCircle } from '@mdi/js'
export default {
name: 'Search',
props: {
@ -43,6 +45,7 @@ export default {
},
data () {
return {
mdiTag, mdiMapMarker, mdiCloseCircle,
tmpfilter: null,
search: ''
}

View file

@ -7,17 +7,20 @@
:left="left"
:right="right"
:timeout="timeout")
v-icon.mr-3(color="white") {{icon}}
v-icon.mr-3(color="white" v-text='icon')
span {{ message }}
template(v-slot:action="{ }")
v-icon(size="16" @click="active = false") mdi-close-circle
v-icon(size="16" @click="active = false" v-text='mdiCloseCircle')
</template>
<script>
import { mdiAlert, mdiCloseCircle, mdiInformation } from '@mdi/js'
export default {
data () {
return {
icon: 'md-alert',
mdiAlert, mdiAlert, mdiCloseCircle, mdiInformation,
icon: mdiInformation,
color: 'secondary',
bottom: true,
top: false,
@ -33,7 +36,7 @@ export default {
this.active = true
this.message = this.$t(message, opts)
this.color = opts.color || 'secondary'
this.icon = opts.icon || 'md-alert'
this.icon = opts.icon || (this.color === 'success' ? mdiInformation : mdiAlert)
}
}
}

View file

@ -17,11 +17,11 @@
v-btn(@click='dialog=false' color='error') {{$t('common.cancel')}}
v-btn(@click='save' color='primary' :disabled='!valid || loading' :loading='loading') {{$t(`common.${editing?'save':'send'}`)}}
v-btn(@click='openDialog' text color='primary') <v-icon>mdi-plus</v-icon> {{$t('common.add')}}
v-btn(@click='openDialog' text color='primary') <v-icon v-text='mdiPlus'></v-icon> {{$t('common.add')}}
v-card-text
v-data-table(
v-if='announcements.length'
:hide-default-footer='announcements.length<10'
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
:headers='headers'
:items='announcements')
template(v-slot:item.actions='{ item }')
@ -36,11 +36,13 @@ import { mapActions } from 'vuex'
import cloneDeep from 'lodash/cloneDeep'
import Editor from '../Editor'
import Announcement from '../Announcement'
import { mdiPlus, mdiChevronRight, mdiChevronLeft } from '@mdi/js'
export default {
components: { Editor, Announcement },
data () {
return {
mdiPlus, mdiChevronRight, mdiChevronLeft,
valid: false,
dialog: false,
editing: false,

View file

@ -5,6 +5,7 @@
v-card-text
v-data-table(
:hide-default-footer='unconfirmedEvents.length<10'
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
:items='unconfirmedEvents'
:headers='headers')
template(v-slot:item.actions='{ item }')
@ -16,6 +17,7 @@
</template>
<script>
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js'
export default {
props: {
@ -23,6 +25,7 @@ export default {
},
data () {
return {
mdiChevronLeft, mdiChevronRight,
valid: false,
dialog: false,
editing: false,
@ -40,7 +43,7 @@ export default {
try {
await this.$axios.$put(`/event/confirm/${event.id}`)
this.$emit('confirmed', event.id)
this.$root.$message('event.confirmed')
this.$root.$message('event.confirmed', { color: 'success' })
} catch (e) {}
},
async remove (event) {

View file

@ -56,25 +56,29 @@
v-btn(color='error' @click='dialogAddInstance=false') {{$t('common.cancel')}}
v-btn(color='primary' :disabled='!valid || loading' :loading='loading' @click='createTrustedInstance') {{$t('common.ok')}}
v-btn.mt-4(@click='dialogAddInstance = true' color='primary' text) <v-icon>mdi-plus</v-icon> {{$t('admin.add_instance')}}
v-btn.mt-4(@click='dialogAddInstance = true' color='primary' text) <v-icon v-text='mdiPlus'></v-icon> {{$t('admin.add_instance')}}
v-data-table(
v-if='settings.trusted_instances.length'
:hide-default-footer='settings.trusted_instances.length<10'
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
:headers='headers'
:items='settings.trusted_instances')
template(v-slot:item.actions="{item}")
v-btn(icon @click='deleteInstance(item)' color='error')
v-icon mdi-delete-forever
v-icon(v-text='mdiDeleteForever')
</template>
<script>
import { mapActions, mapState } from 'vuex'
import get from 'lodash/get'
import axios from 'axios'
import { mdiDeleteForever, mdiPlus, mdiChevronLeft, mdiChevronRight } from '@mdi/js'
export default {
name: 'Federation',
data ({ $store, $options }) {
return {
mdiDeleteForever, mdiPlus, mdiChevronLeft, mdiChevronRight,
instance_url: '',
instance_name: $store.state.settings.instance_name,
instance_place: $store.state.settings.instance_place,
@ -127,8 +131,8 @@ export default {
key: 'trusted_instances',
value: this.settings.trusted_instances.concat({
url: this.instance_url,
name: instance.data.metadata.nodeName,
label: instance.data.metadata.nodeLabel
name: get(instance, 'data.metadata.nodeName', ''),
label: get(instance, 'data.metadata.nodeLabel', '')
})
})
this.$refs.form.reset()

View file

@ -10,10 +10,11 @@
:items-per-page='5'
:search='instancesFilter'
:hide-default-footer='instances.length<5'
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
dense :headers='instancesHeader'
@click:row='instanceSelected')
template(v-slot:item.blocked="{ item }")
v-icon(@click='toggleBlock(item)') {{item.blocked ? 'mdi-checkbox-intermediate' : 'mdi-checkbox-blank-outline'}}
v-icon(@click='toggleBlock(item)' v-text='item.blocked ? mdiCheckboxIntermediate : mdiCheckboxBlankOutline')
v-col(:span='11')
span {{$t('common.users')}}
@ -22,45 +23,53 @@
:items-per-page='5'
:search='usersFilter'
:hide-default-footer='users.length<5'
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
dense :headers='usersHeader')
template(v-slot:item.blocked="{ item }")
v-icon(@click='toggleUserBlock(item)') {{item.blocked?'mdi-checkbox-intermediate':'mdi-checkbox-blank-outline'}}
v-icon(@click='toggleUserBlock(item)' v-text='item.blocked ? mdiCheckboxIntermediate : mdiCheckboxBlankOutline')
div
v-card-title {{$t('common.resources')}}
v-data-table(:items='resources' dense
:headers='resourcesHeader'
:hide-default-footer='resources.length<10'
:items-per-page='10')
:items-per-page='10'
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }')
template(v-slot:item.content='{ item }')
span(v-html='item.data.content')
template(v-slot:item.created='{ item }')
span {{item.created | dateFormat('lll')}}
template(v-slot:item.user='{ item }')
span {{item.ap_user.preferredUsername}}
a(:href='item.ap_user.url || item.ap_user.ap_id' target='_blank') {{item.ap_user.preferredUsername}}
template(v-slot:item.event='{ item }')
span {{item.event.title}}
nuxt-link(:to='`/event/${item.event.slug || item.event.id}`') {{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-icon(v-text='mdiDotsVertical')
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-title <v-icon left v-text='mdiEyeOff'></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-title <v-icon left v-text='mdiEye'></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-title <v-icon left v-text='mdiDelete'></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'
import { mdiDelete, mdiEye, mdiEyeOff, mdiDotsVertical, mdiCheckboxIntermediate,
mdiCheckboxBlankOutline, mdiChevronLeft, mdiChevronRight } from '@mdi/js'
export default {
name: 'Moderation',
data () {
return {
mdiDelete, mdiEye, mdiEyeOff, mdiDotsVertical, mdiCheckboxIntermediate,
mdiCheckboxBlankOutline, mdiChevronLeft, mdiChevronRight,
instances: [],
resources: [],
users: [],

View file

@ -1,6 +1,11 @@
<template lang='pug'>
v-container
v-card-title {{$t('common.places')}}
v-spacer
v-text-field(v-model='search'
:append-icon='mdiMagnify' outlined rounded
label='Search'
single-line hide-details)
v-card-subtitle(v-html="$t('admin.place_description')")
v-dialog(v-model='dialog' width='600' :fullscreen='$vuetify.breakpoint.xsOnly')
@ -29,19 +34,26 @@
v-card-text
v-data-table(
:headers='headers'
:items='places')
:items='places'
:hide-default-footer='places.length<5'
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
:search='search')
template(v-slot:item.actions='{item}')
v-btn(@click='editPlace(item)' color='primary' icon)
v-icon mdi-pencil
v-icon(v-text='mdiPencil')
</template>
<script>
import { mapState, mapActions } from 'vuex'
import { mdiPencil, mdiChevronLeft, mdiChevronRight } from '@mdi/js'
export default {
data () {
return {
mdiPencil, mdiChevronRight, mdiChevronLeft,
loading: false,
dialog: false,
valid: false,
search: '',
place: { name: '', address: '', id: null },
headers: [
{ value: 'name', text: 'Name' },

View file

@ -3,7 +3,6 @@
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 )"
@ -53,7 +52,7 @@ export default {
async testSMTP () {
this.loading = true
try {
this.setSetting({ key: 'smtp', value: this.smtp })
await 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) {
@ -68,7 +67,9 @@ export default {
}
},
done () {
if (this.smtp.auth.pass) {
this.setSetting({ key: 'smtp', value: JSON.parse(JSON.stringify(this.smtp)) })
}
this.$emit('close')
},

View file

@ -53,9 +53,9 @@
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-icon v-if='showSMTPAlert' color='error' v-text='mdiAlert'></v-icon> {{$t('admin.show_smtp_setup')}}
v-btn(text @click='$emit("complete")' color='primary' v-if='setup') {{$t('common.next')}}
v-icon mdi-arrow-right
v-icon(v-text='mdiArrowRight')
</template>
@ -65,6 +65,7 @@ import { mapActions, mapState } from 'vuex'
import moment from 'dayjs'
import tzNames from './tz.json'
import locales from '../../locales/esm'
import { mdiAlert, mdiArrowRight } from '@mdi/js'
export default {
props: {
@ -74,6 +75,7 @@ export default {
name: 'Settings',
data ({ $store }) {
return {
mdiAlert, mdiArrowRight,
title: $store.state.settings.title,
description: $store.state.settings.description,
locales: Object.keys(locales).map(locale => ({ value: locale, text: locales[locale] })),
@ -83,7 +85,7 @@ export default {
computed: {
...mapState(['settings']),
showSMTPAlert () {
return !this.setup && (!this.settings.admin_email || !this.settings.smtp || !this.settings.smtp.host || !this.settings.smtp.user)
return !this.setup && (!this.settings.admin_email || !this.settings.smtp || !this.settings.smtp.host || !this.settings.smtp.auth.user)
},
instance_locale: {
get () { return this.settings.instance_locale },

View file

@ -8,7 +8,7 @@
@change='uploadLogo'
accept='image/*')
template(slot='append-outer')
v-btn(color='warning' text @click='resetLogo') <v-icon>mdi-restore</v-icon> {{$t('common.reset')}}
v-btn(color='warning' text @click='resetLogo') <v-icon v-text='mdiRestore'></v-icon> {{$t('common.reset')}}
v-img(:src='`${settings.baseurl}/logo.png?${logoKey}`'
max-width="60px" max-height="60px" contain)
@ -50,8 +50,8 @@
v-card-title {{$t('admin.footer_links')}}
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-btn(color='primary' text @click='openLinkModal') <v-icon v-text='mdiPlus'></v-icon> {{$t('admin.add_link')}}
v-btn(color='warning' text @click='reset') <v-icon v-text='mdiRestore'></v-icon> {{$t('common.reset')}}
v-card
v-list.mt-1(two-line subheader)
v-list-item(v-for='link in settings.footerLinks'
@ -61,16 +61,18 @@
v-list-item-subtitle {{link.href}}
v-list-item-action
v-btn(icon color='error' @click.stop='removeFooterLink(link)')
v-icon mdi-delete-forever
v-icon(v-text='mdiDeleteForever')
</template>
<script>
import { mapActions, mapState } from 'vuex'
import { mdiDeleteForever, mdiRestore, mdiPlus } from '@mdi/js'
export default {
name: 'Theme',
data () {
return {
mdiDeleteForever, mdiRestore, mdiPlus,
valid: false,
logoKey: 0,
link: { href: '', label: '' },

View file

@ -3,11 +3,11 @@
v-card-title {{$t('common.users')}}
v-spacer
v-text-field(v-model='search'
append-icon='mdi-magnify' outlined rounded
:append-icon='mdiMagnify' outlined rounded
label='Search'
single-line hide-details)
v-btn(color='primary' text @click='newUserDialog = true') <v-icon>mdi-plus</v-icon> {{$t('common.new_user')}}
v-btn(color='primary' text @click='newUserDialog = true') <v-icon v-text='mdiPlus'></v-icon> {{$t('common.new_user')}}
//- ADD NEW USER
v-dialog(v-model='newUserDialog' :fullscreen='$vuetify.breakpoint.xsOnly')
@ -20,7 +20,7 @@
:label="$t('common.email')"
:rules="$validators.email")
v-switch(v-model='new_user.is_admin' :label="$t('common.admin')" inset)
v-alert(type='info' :closable='false') {{$t('admin.user_add_help')}}
v-alert(type='info' :closable='false' :icon='mdiInformation') {{$t('admin.user_add_help')}}
v-card-actions
v-spacer
v-btn(@click='newUserDialog=false' color='error') {{$t('common.cancel')}}
@ -32,10 +32,11 @@
:headers='headers'
:items='users'
:hide-default-footer='users.length<5'
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
:search='search')
template(v-slot:item.is_active='{item}')
v-icon(v-if='item.is_active' color='success') mdi-check
v-icon(v-else color='warning') mdi-close
v-icon(v-if='item.is_active' color='success' v-text='mdiCheck')
v-icon(v-else color='warning' v-text='mdiClose')
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)'
@ -49,6 +50,7 @@
<script>
import { mapState } from 'vuex'
import get from 'lodash/get'
import { mdiClose, mdiMagnify, mdiCheck, mdiPlus, mdiInformation, mdiChevronLeft, mdiChevronRight } from '@mdi/js'
export default {
name: 'Users',
@ -57,6 +59,7 @@ export default {
},
data () {
return {
mdiClose, mdiMagnify, mdiCheck, mdiPlus, mdiInformation, mdiChevronLeft, mdiChevronRight,
newUserDialog: false,
valid: false,
new_user: {

View file

@ -1,10 +1,34 @@
## Get events
GET
{: .label .label-green}
**`/api/events`**
**Params**
| start | `integer` | start timestamp (default: now) |
| end | `integer` | end timestamp (optional) |
| tags | `array` | List of tags |
| places | `array` | List of places |
| max | `integer` | Max events |
| show_recurrent | `boolean` | Show also recurrent events (default: as choosen in admin settings) |
***Example***
[https://demo.gancio.org/api/events](https://demo.gancio.org/api/events)
[usage example](https://framagit.org/les/gancio/-/blob/master/webcomponents/src/GancioEvents.svelte#L18-42)
---
## Add a new event
POST
{: .label .label-orange}
**`/event`**
**`/api/event`**
> info "info"
> `Content-Type` has to be `multipart/form-data` to support image upload
@ -21,7 +45,6 @@ POST
| tags | `array` | List of tags |
| recurrent | `object` | Recurrent event details |
| recurrent.frequency | `string` | could be `1w` or `2w` |
| recurrent.type | `string` | not used |
| recurrent.days | `array` | array of days |
| image | `image` | Image |
@ -41,7 +64,7 @@ GET
**Response**
```json
{
{
"description" : null,
"recover_code" : "",
"id" : 1,
@ -51,7 +74,7 @@ GET
"settings" : "{}",
"email" : "eventi@cisti.org",
"is_admin" : true
}
```
}
```
---

View file

@ -1,30 +1,30 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (6.0.3.7)
activesupport (6.0.4.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
zeitwerk (~> 2.2, >= 2.2.2)
addressable (2.7.0)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
colorator (1.1.0)
concurrent-ruby (1.1.8)
em-websocket (0.5.2)
concurrent-ruby (1.1.9)
em-websocket (0.5.3)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
http_parser.rb (~> 0)
eventmachine (1.2.7)
ffi (1.15.1)
ffi (1.15.4)
forwardable-extended (2.6.0)
gemoji (3.0.1)
html-pipeline (2.14.0)
activesupport (>= 2)
nokogiri (>= 1.4)
http_parser.rb (0.6.0)
i18n (1.8.10)
http_parser.rb (0.8.0)
i18n (1.8.11)
concurrent-ruby (~> 1.0)
jekyll (4.2.0)
jekyll (4.2.1)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
@ -62,28 +62,24 @@ GEM
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
liquid (4.0.3)
listen (3.5.1)
rb-fsevent (~> 0.10, >= 0.10.3)
listen (3.7.0)
rb-inotify (~> 0.9, >= 0.9.10)
mercenary (0.4.0)
mini_magick (4.11.0)
mini_portile2 (2.5.3)
minitest (5.14.4)
nokogiri (1.11.7)
mini_portile2 (~> 2.5.0)
nokogiri (1.12.5-x86_64-linux)
racc (~> 1.4)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
premonition (4.0.1)
jekyll (>= 3.7, < 5.0)
public_suffix (4.0.6)
racc (1.5.2)
rake (13.0.3)
rb-fsevent (0.11.0)
racc (1.6.0)
rake (13.0.6)
rb-inotify (0.10.1)
ffi (~> 1.0)
rexml (3.2.5)
rouge (3.26.0)
rouge (3.26.1)
safe_yaml (1.0.5)
sassc (2.4.0)
ffi (~> 1.9)
@ -92,14 +88,14 @@ GEM
thread_safe (0.3.6)
tzinfo (1.2.9)
thread_safe (~> 0.1)
tzinfo-data (1.2021.1)
tzinfo-data (1.2021.5)
tzinfo (>= 1.0.0)
unicode-display_width (1.7.0)
unicode-display_width (1.8.0)
wdm (0.1.1)
zeitwerk (2.4.2)
zeitwerk (2.5.1)
PLATFORMS
ruby
x86_64-linux
DEPENDENCIES
jekyll
@ -114,4 +110,4 @@ DEPENDENCIES
wdm (~> 0.1.0)
BUNDLED WITH
2.1.4
2.2.27

File diff suppressed because it is too large Load diff

View file

@ -8,12 +8,77 @@ nav_order: 10
All notable changes to this project will be documented in this file.
### 1.4.3 - 10 mar '22
- fix [#140](https://framagit.org/les/gancio/-/issues/140) - Invalid date
- fix [#141](https://framagit.org/les/gancio/-/issues/141) - Cannot change logo
- fix same day events
- add missing icons in admin
- prepare multisite settings
- improve initialization
- start unit testing API (it's never too late)
- make it clear that the admin password must be saved at the end of setup
### 1.4.1 - 4 mar '22
- add gl/galego locale, thanks @xosem
- fix import redirect loop
- add missing icons (close, repeat, arrows ...)
- turn rss icon into a real link to improve a11y
- force seconds to 0 for each events, fix recurring events starting date issue
- fix next/prev selection on same datetime events
- improve moderation UI (add author and event link + format creation date)
- refactoring resource UI from fedi
### 1.4.0 - 9 feb '22
- improve Cumulative Layout Shift
- remove filename as default media label to avoid leak metadata
- add endData to microdata
- security fix with filtering settings, avoid sharing SMTP pass with front-end
- fix broken SMTP
- remove global materialicons / vuetify css, use threeshake and @nuxt/vuetify (really improve lighthouse score)
- new Dockerfile using node:17.4-slim as base img (from 1.5Gb to ~800Mb)
- add XSS and path traversal mitigation
- improve a11y
- update deps
### 1.3.3 - 1 feb '22
- security fix, avoid sharing smtp pass with front-end
### 1.3.2 - 1 feb '22
- fix webcomponent for event without img
### 1.3.1 - 1 feb '22
- inherits tags in recurring events [#138](https://framagit.org/les/gancio/-/issues/138)
- you can now skip an occurrence of a recurring event
- fix `show_recurrent` event in webcomponent and API
- add new webcomponent `sidebar` attribute and a [`fullwith` layout](https://gancio.org/usage/embed#embed-event-lists)
### 1.3.0 - 26 gen '22
- add mariadb support
- add [microdata](https://developer.mozilla.org/en-US/docs/Web/HTML/Microdata) support
- support db setup via environment variables (used in updated `docker-compose.yml` files)
- improve rss feed:
- fix validation
- add enclusure media for featured image
- add categories
- fix typo in export code
- add theme attribute to gancio-events webcomponent (dark/light)
- increase thumbs quality
- improve logo for light theme
- improve [wordpress plugin](https://wordpress.org/plugins/wpgancio/)
- add \[gancio-event\] and \[gancio-events\] shortcode
- allow gancio-events / gancio-event tags in editor
- automatically enqueue webcomponent script
- tags/places filters are now inclusive not exclusive
- fix image undefined alternative text
- update documentation, dependencies, translations
### 1.2.2 - 7 dic '21
- shiny new gancio-event\[s\] webcomponents => [docs](https://gancio.org/usage/embed)
- new backend plugin system
- new backend plugin system => [docs](https://gancio.org/dev/plugins)
- improve media focal point selection
- improve non-js experience (load img, use native lazy loading)
- improve user_confirm / recover code flow
- permit admins to choose user password (usefull on instance without SMTP configuration)
- 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)

View file

@ -10,15 +10,37 @@ nav_order: 8
1. TOC
{:toc}
## Get events
GET
{: .label .label-green}
**`/api/events`**
**Params**
| start | `integer` | start timestamp (default: now) |
| end | `integer` | end timestamp (optional) |
| tags | `array` | List of tags |
| places | `array` | List of places |
| max | `integer` | Max events |
| show_recurrent | `boolean` | Show also recurrent events (default: as choosen in admin settings) |
***Example***
[https://demo.gancio.org/api/events](https://demo.gancio.org/api/events)
[usage example](https://framagit.org/les/gancio/-/blob/master/webcomponents/src/GancioEvents.svelte#L18-42)
---
## Add a new event
POST
{: .label .label-orange}
**`/event`**
**`/api/event`**
> info "info"
> `Content-Type` has to be `multipart/form-data` to support image upload
@ -35,7 +57,6 @@ POST
| tags | `array` | List of tags |
| recurrent | `object` | Recurrent event details |
| recurrent.frequency | `string` | could be `1w` or `2w` |
| recurrent.type | `string` | not used |
| recurrent.days | `array` | array of days |
| image | `image` | Image |
@ -55,7 +76,7 @@ GET
**Response**
```json
{
{
"description" : null,
"recover_code" : "",
"id" : 1,
@ -65,7 +86,7 @@ GET
"settings" : "{}",
"email" : "eventi@cisti.org",
"is_admin" : true
}
```
}
```
---

51
docs/dev/plugin.md Normal file
View file

@ -0,0 +1,51 @@
---
layout: default
title: Plugins
permalink: /dev/plugins
nav_order: 2
parent: Hacking
---
## Plugins
Since **v.1.2.2** you can write your own plugin that react to event related action (create,update,delete).
> info "What this is useful for?"
> - Do you want to create a post in your wordpress website each time an event is published? [hint](http://wp-api.org/node-wpapi/using-the-client/#creating-posts)
> - Do you want to send a summary notification of daily events via mail?
> - Notify a telegram group or share via twitter?
>
> [**<u>Please share your plugins or your needs</u>**](/contacts)
Plugins should be inside `./plugins` directory, this is an example:
```js
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
```

View file

@ -1,7 +1,7 @@
FROM node:buster
FROM node:17.4-slim
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
RUN yarn global add --latest --production --silent https://gancio.org/latest.tgz
ADD entrypoint.sh /
RUN chmod 755 /entrypoint.sh
ENTRYPOINT [ "/bin/sh", "/entrypoint.sh" ]

View file

@ -0,0 +1,20 @@
version: '3'
services:
gancio:
build: .
restart: always
image: node:17.4-slim
container_name: gancio
environment:
- PATH=$PATH:/home/node/.yarn/bin
- GANCIO_DATA=/home/node/data
- NODE_ENV=production
- GANCIO_DB_DIALECT=sqlite
- GANCIO_DB_STORAGE=./gancio.sqlite
entrypoint: /entrypoint.sh
command: gancio start --docker
volumes:
- ./data:/home/node/data
ports:
- "127.0.0.1:13120:13120"

View file

@ -0,0 +1,37 @@
version: '3'
services:
db:
image: mariadb
container_name: mariadb
volumes:
- ./db:/var/lib/mysql
- /etc/localtime:/etc/localtime:ro
environment:
- MARIADB_USER=gancio
- MARIADB_DATABASE=gancio
- MARIADB_PASSWORD=gancio
- MARIADB_RANDOM_ROOT_PASSWORD=yes
restart: always
gancio:
build: .
restart: always
image: node:17.4-slim
container_name: gancio
environment:
- PATH=$PATH:/home/node/.yarn/bin
- GANCIO_DATA=/home/node/data
- NODE_ENV=production
- GANCIO_DB_DIALECT=mariadb
- GANCIO_DB_HOST=db
- GANCIO_DB_DATABASE=gancio
- GANCIO_DB_USERNAME=gancio
- GANCIO_DB_PASSWORD=gancio
command: gancio start --docker
entrypoint: /entrypoint.sh
volumes:
- ./data:/home/node/data
ports:
- "127.0.0.1:13120:13120"
depends_on:
- db

View file

@ -18,12 +18,17 @@ services:
gancio:
build: .
restart: always
image: node:buster
image: node:17.4-slim
container_name: gancio
environment:
- PATH=$PATH:/home/node/.yarn/bin
- GANCIO_DATA=/home/node/data
- NODE_ENV=production
- GANCIO_DB_DIALECT=postgres
- GANCIO_DB_HOST=db
- GANCIO_DB_DATABASE=gancio
- GANCIO_DB_USERNAME=gancio
- GANCIO_DB_PASSWORD=gancio
command: gancio start --docker
entrypoint: /entrypoint.sh
volumes:

View file

@ -4,12 +4,14 @@ services:
gancio:
build: .
restart: always
image: node:buster
image: node:17.4-slim
container_name: gancio
environment:
- PATH=$PATH:/home/node/.yarn/bin
- GANCIO_DATA=/home/node/data
- NODE_ENV=production
- GANCIO_DB_DIALECT=sqlite
- GANCIO_DB_STORAGE=./gancio.sqlite
entrypoint: /entrypoint.sh
command: gancio start --docker
volumes:

View file

@ -1,33 +1,100 @@
---
layout: default
title: Embed events
title: Embed events in webpages
permalink: /usage/embed
nav_order: 1
parent: Usage
---
## Embed event
## Embed a single event or a list of events in your webpage
{: .no_toc }
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
1. TOC
{:toc}
## Webcomponents
[Webcomponents](https://www.webcomponents.org/introduction) usage requires a small (~5kB gzipped) js script to be loaded in your page (note that you should use your instance name):
```html
<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**
### Embed a single event
To embed an event in webpages you use `<gancio-event>` custom element, you can copy the required code in **event's page > Embed > Copy**, should be like the following:
<script src='https://demo.gancio.org/gancio-events.es.js'></script>
<gancio-event id=17 baseurl='https://demo.gancio.org'></gancio-event>
```javascript
```html
<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>
<script src='/assets/js/gancio-events.es.js'></script>
<gancio-event id=17 baseurl='https://demo.gancio.org'></gancio-event>
### Embed event lists
You can also embed a list of events using `<gancio-events>` custom element, you can copy the required code in **Export > List > Copy**
```html
<gancio-events baseurl='https://gancio.cisti.org'>
<a href='https://gancio.cisti.org'>Gancio Events</a>
</gancio-events>
```
<script>
var theme = 'light';
var sidebar = 'true';
function toggleDark() {
var items = document.getElementsByTagName('gancio-events');
theme = theme === 'dark' ? 'light' : 'dark';
items[0].setAttribute('theme', theme );
}
function toggleSidebar() {
var items = document.getElementsByTagName('gancio-events');
sidebar = sidebar === 'true' ? 'false' : 'true';
items[0].setAttribute('sidebar', sidebar );
}
function changeMax(value) {
var items = document.getElementsByTagName('gancio-events');
items[0].setAttribute('maxlength', value);
}
function changeTitle (title) {
var items = document.getElementsByTagName('gancio-events');
items[0].setAttribute('title', title)
}
</script>
<style>
label {
display: block;
margin: 0px;
padding: 10px;
}
</style>
<label><input type='checkbox' onchange="toggleDark()"/> Dark</label>
<label><input type='checkbox' checked onchange="toggleSidebar()"/> Sidebar</label>
<label>Max items <input value=4 type='number' label='Max items' onchange="changeMax(this.value)"/></label>
<label>Title <input value='Gancio' type='text' onkeyup="changeTitle(this.value)"/></label>
<gancio-events sidebar='true' title='Gancio' theme='light' maxlength=4 baseurl='https://gancio.cisti.org'><a href='https://gancio.cisti.org'>Gancio Events</a></gancio-events>
> info "Customize"
> Note that you can modify the title (or completely remove it using an empty `title` param),
> you can limit the list to a maximum number of events using the `maxlength` parameter and filter events by `tags` or `places` using that parameters (it's easier using **gancio** than to explain it here)
## IFrame
You can also use the old iframe method
<iframe src='https://demo.gancio.org/embed/17' style="width: 410px; border: none; height: 210px; overflow: hidden;"></iframe>
```html
<iframe src='https://demo.gancio.org/embed/17' style="width: 410px; border: none; height: 210px; overflow: hidden;"></iframe>
```
## Wordpress
To embed an event or a list of events into a [WordPress](https://wordpress.com) website you can use the [WPGancio](https://wordpress.org/plugins/wpgancio/) plugin, this allows you to use webcomponents and shortcodes and automatically includes the needed script in each page and post.

View file

@ -18,22 +18,22 @@ A shared agenda for local communities.
## Some relevant key features:
- **Focus on content** not on people:
nowhere on gancio appears the identity of who published the event, not even under a nickname, not even to administrators (except in the db). This is not an ego-friendly platform.
nowhere on gancio does the identity of who posted an event appear, not even under a nickname, not even to administrators (except in the db). This is not an ego-friendly platform.
- **Visitors first**. We do not want logged user to get more features than random visitor. We do not want users to register, except to publish events and even in this case you can publish an anonymous event.
- **Visitors first**. We do not want logged user to get more features than random visitor. We don't want users to register, except to post events and even then you can post an anonymous event.
- **Anonymous events**: optionally a visitor can create events without being registered (an administrator has to confirm them)
- **Anonymous events**: optionally a visitor can create events without being registered (an administrator must confirm them)
- **We are not interested in making hits** so we export events in many ways, via RSS feeds, via global or individual ics, incorporating lists of events or single event via iframe on other websites and via [AP]({% link federation.md %})
- **We don't care about making hits** so we export events in many ways: via RSS feeds, via global or individual ics, allowing you to embed list of events or single event via [iframe or webcomponent]({% link embed.md %}) on other websites, via [AP]({% link federation.md %}), [microdata](https://developer.mozilla.org/en-US/docs/Web/HTML/Microdata) and [microformat](https://developer.mozilla.org/en-US/docs/Web/HTML/microformats#h-event)
- Very easy UI
- Multidays events support (festival, conferences...)
- Recurrent events support (each monday, each two monday, each monday and friday, each two saturday, etc.)
- Filters events for tags or places
- Multi-day events (festival, conferences...)
- Recurring events (each monday, each two monday, each monday and friday, each two saturday, etc.)
- Filter events for tags or places
- RSS and ICS export (with filters)
- embeddable iframe ([example](https://gancio.cisti.org/embed/list?title=Upcoming events))
- embed your events in your website with [webcomponents]({% link embed.md %}) or iframe ([example](https://gancio.cisti.org/embed/list?title=Upcoming events))
- boost / bookmark / comment events from the fediverse!
- Lot of configurations available (user registration open/close, enable federation, enable recurrent events)
- Lot of configurations available (dark/light theme, user registration open/close, enable federation, enable recurring events)
### License

View file

@ -23,7 +23,7 @@ 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)
$(ls -d config.json uploads user_locale db.sqlite gancio.dump postgres data db logs 2> /dev/null)
```
> info "Automatic backup"

View file

@ -7,7 +7,7 @@ parent: Install
## Configuration
{: .no_toc }
`gancio` configuration is done during installation process but you can change it editing the configuration file. Note that you can always re-run gancio with `--setup` flag to use the interactive setup.
`gancio` configuration is done during installation process but you can change it editing the configuration file.
The configuration file shoud be a `.json` or a `.js` file and could be specified using the `--config` flag.
- <small>eg. `gancio start --config ./config.json`</small>
@ -27,7 +27,7 @@ This probably support unix socket too
```
- ### Database
DB configuration, look [here](https://sequelize.org/master/class/lib/sequelize.js~Sequelize.html#instance-constructor-constructor) for options.
DB configuration, look [here](https://sequelize.org/v6/class/src/sequelize.js~Sequelize.html#instance-constructor-constructor) for options.
```json
"db": {
"dialect": "sqlite",

View file

@ -21,8 +21,8 @@ sudo npm install -g yarn
```
<small>[source](https://github.com/nodesource/distributions/blob/master/README.md)</small>
1. Setup with postgreSQL __(optional as you can choose sqlite)__
#### Choose you database (sqlite, postgresql, mariadb, mysql)
1. Setup using postgreSQL __(optional)__
```bash
sudo apt-get install postgresql
# Create the database
@ -32,13 +32,25 @@ postgres=# create user gancio with encrypted password 'gancio';
postgres=# grant all privileges on database gancio to gancio;
```
1. Setup using MariaDB (__optional__)
```bash
sudo apt-get install mariadb
sudo mysql
MariaDB [(none)]> create database gancio;
Query OK, 1 row affected (0.001 sec)
MariaDB [(none)]> create user gancio identified by 'gancio';
Query OK, 0 rows affected (0.011 sec)
MariaDB [(none)]> grant all privileges on gancio.* to gancio;
Query OK, 0 rows affected (0.009 sec)
```
1. Create a user to run gancio from
```bash
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
sudo yarn global add --silent {{site.url}}/latest.tgz
```
1. Setup systemd service and reload systemd
@ -66,6 +78,6 @@ sudo systemctl start gancio
```bash
yarn global remove gancio
yarn cache clean
yarn global add --silent {{site.url}}/latest.tgz 2> /dev/null
sudo service gancio restart
yarn global add --silent {{site.url}}/latest.tgz
sudo systemctl restart gancio
```

View file

@ -11,76 +11,51 @@ nav_order: 2
1. TOC
{:toc}
## Initial setup
- __You must have the following dependencies installed: Docker, Docker Compose and Nginx__
## Setup
Make sure to have [Docker Engine](https://docs.docker.com/engine/install/),
[Docker Compose](https://docs.docker.com/compose/install/) and [nginx](https://nginx.org/en/docs/install.html) installed:
```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__
Create a directory where everything related to gancio is stored:
```bash
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**
Download `Dockerfile` and `entrypoint.sh`:
```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
```
docker-compose build
```
</div>
## Use postgreSQL
<div class='code-example bg-grey-lt-100' markdown="1">
1. **Download docker-compose.yml and Dockerfile**
Download `docker-compose.yml` choosing your preferred database dialect between `sqlite`, `postgres` and `mariadb`:
```bash
wget {{site.url}}{% link /docker/Dockerfile %}
wget {{site.url}}{% link /docker/entrypoint.sh %}
wget {{site.url}}{% link /docker/postgres/docker-compose.yml %}
DB=sqlite
wget {{site.url}}/docker/$DB/docker-compose.yml
```
1. Build docker image
```
Build docker image
```bash
docker-compose build
```
</div>
## Start gancio
1. Run your container
Start your container:
```bash
docker-compose up -d
```
1. Look at logs
You can take a look at logs using:
```bash
tail -f data/logs/gancio.log
```
1. [Setup nginx as a proxy]({% link install/nginx.md %})
You'll need to [setup nginx as a proxy]({% link install/nginx.md %}) then you can point your web browser to your domain :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.
## Upgrade
@ -89,20 +64,6 @@ tail -f data/logs/gancio.log
> Don't be lazy and [backup]({% link install/backup.md %}) your data!
> error "Upgrade from a version < 1.0"
> Since v1.0 our docker setup is changed and a new container has to be built:
>
> 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`
> 1. Stop your old container `docker-compose stop`
> 1. Start your new container `docker-compose up`
```bash
cd /opt/gancio
docker-compose up -d --no-deps --build

View file

@ -9,7 +9,7 @@ has_toc: false
## 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
- optionally an SMTP server to deliver emails
## Install

View file

@ -8,7 +8,10 @@ nav_order: 7
- [gancio.cisti.org](https://gancio.cisti.org) (Turin, Italy)
- [lapunta.org](https://lapunta.org) (Florence, Italy)
- [sapratza.in](https://sapratza.in/) (Sardinia, Italy)
- [termine.161.social](https://termine.161.social) (Germany)
- [ezkerraldea.euskaragendak.eus](https://ezkerraldea.euskaragendak.eus/)
- [lakelogaztetxea.net](https://lakelogaztetxea.net)
<small>Do you want your instance to appear here? [Write us]({% link contact.md %}).</small>

83
docs/public.key Normal file
View file

@ -0,0 +1,83 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFjwEUkBEADGkRYEgSGQHEi/YIKvJCuDDB8TYGT3Gzuz0BfgwWlSiupjzkTv
eJAZTX+rH1MCnlwV7QJ+Y7v2I/REA9acc+8IapvcUarBJsEwtdp9nIyjKnDR5eiF
UOkh4BxLsi541cm6UreSPOMGEEviTiv4CXYeIg+V+Q20Sp+2dy8vwBe3xSYW5aVg
c+d6IFeq40CtYyqheAPpJKQop/DKtRRklz3bIOiObXHIYZNzjCDSQyjbBAF1DapS
4Z6pqR4+6l5+ylXbWGI7Cg+JFmGl4jH1zI3lX5utWbqsIS/cDkLG+pYNwKDoP8o9
VY7SSQbr/n8BaPRNsLP6zw3xLok0+i8GGYUyeNN0Yua3IIB8Bu503+4kZTgl2J3G
sxf+zGS6cWA6u9ROo+plDN9fip7XW29hfVCjc7lsdsbvVGHLI2ZScF0gfXuI2AMf
kLeoI32ZMlPT6BSWmYO0fAoZyZgIG7u+DH59pEqM1mc4ChSzhSxtDDzsOenbAstH
xkJWB9Y/P77OfmwiNkUkCBb7uk/n092g/0xf8CqE82sj2P4LccBt0J1qi4wSyAZQ
Nfm2I5dVmW9A21AoLjuNoTLbB0EEcORyrYYcA13b3DIj0UcSlo/KsP0ZZe6Xu915
smUixqWJ8j9FPGAnIGY7FJEkHmnhXc/wj+qVxPFKgMfDvbdtIXIzT6eMFwARAQAB
tDJVbmRlcnNjb3JlIF9UTyogSGFja2xhYiA8dW5kZXJzY29yZUBhdXRpc3RpY2ku
b3JnPokCNwQTAQgAIQUCWPARSQIbAwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAK
CRC7603TmsbMqQUSD/4p0WHK51l4SULlAGYVUCd8gM/OGfvA8FkzBJaPwvGAJTGL
Pj9U876R4MxBSxsI0UhajKDQBRlswpSOxTGknBVDJ9O1RPhXYgRPQt+piiiGIExT
n3be+lxw8DDj/t5MGQDXMbXR/UfVlO5X573/2XSmHVWB9RWdfL3XYGYusxPB1kLO
jSeC8ETcV/tka8kToPFW8Fshg8xVkBkzvXiwXrbdKwreDAo/Ukms+8MkI8Qlyhhh
vfiUb+GnGuEnHSUom7Sn2YMW5qkOKsVZ4Ca5DhqjbXZEDCA2VhnaS5TrRaBb0odk
B9rg399pI36cOAO9qtejchG88JbK6Tr7Dh56/Lr6T0mZBPUWk4nJLAWcF6U5k9kT
jVD8K9zsGL/4Zex0a7GCcS0bVmXPt8+G9n4VoFMP5rjVkE9Mi7ugBzMNzEDd2crT
GMjPpe77arfEaLfg/2mnDCmqz3osRqV0L+Ef3S4GNjmT0LNiEWbB7eGEOVf2CEBb
IHoexd74DF6Dz96tjaN7xeDgvm/hYLdioyCgf6cccZp4xdE2SVcEWOLYoG5B77qr
W4b+ssOotx7U0pEpn9A+sYno73tHfzNhoC1UXMcaVh7+nWJAQ0AhVkF6EHUXlb7J
p1SDmtUajBYeOEfWDfJZ/3p9Z4ARiE/CEo08O59xtJ5p8XNxBEU1TbpNADHdm4kC
HAQQAQgABgUCWPAVQQAKCRA1KRglCwEhd/ciD/9k4fxrgH4ZkEbjwZ4r7HrZ8A29
ZsP/ThAaSiLgoKCDjpes2hYir8jwp5QI3aEINseL4+Uzh9st6y3WthgH6aTTPUvP
QTjpLfre16YTTWS7ztfLMGUGvXpruXncF8rQUeRwjc/FK0/yzMFcY8iX9DpSSKSc
evNlfbDblzQhzo0ibEQ5t49HsaFXNgBlY74PrdFrAde8gI5PPaBWJcg94s1hsCx5
Epv7Vfl/wQI8mcnRBAfKfnqVl3AYoqwwCFFuxhdjEyNz35otWJgBfufd5jFVmY8a
dT5ZDieFabOdcp0qlGQaPkXO9cIMkbloIxMFiID8kArn4cEWx/hEHa6iHqcBhXgN
hZqiTONYlaRZGcWyRh+hFOzVfVdgy/Q7Mqo6VRqbuWWAdMJUrT29xuL7HP2M+Kd6
/Fk3C0cfupw7sjhcPlWq/rhvLM+rjfury6cCpGP5Ff22VZebddgGuOGOcY09Q+p+
nUaEcU2x/p/JWwyZC8j0cDTBEvzOHZDi2avs3rYjydkbi2syRn0IslbRnrKV45tw
amKm9vaI8CWXzvEaGfAUclwBcXw5wES/x2h0Mt6CR2Kt5zNnbieSXjWN/5YLQpQ1
YcHEsVdB1lv9jsFzls1xg9hFyLx7DuIsCrZSMQ1wiBWHkvmU9DCH9sCN4KFzI58Q
hexGgVf6e8ACRaUsoYkCMwQQAQgAHRYhBLSjt7RWdzvKZ9Uz1CZCVK0JvPrZBQJY
/SKHAAoJECZCVK0JvPrZVIUP/2+EB2MCFQRUq1jS7Ud7zQVVneCj1jpBoQqrQOa3
ntfIwr0hIJkAuNqsObkgVtS5GThW5w1x0d6MGvSD80n18ipKzsfp7XLU2FBUwU6D
nHgUuTNe092aIXPhln/qIRA7EieCSTyvAvE6P+d/VlhylY/R97TN13jhYz+3wBV0
IQ4fkfgU08N9MGJ/cBk8CQQAPM+3tDcWQCyefaOVX1JW93gROwcUeRSYO1qQVwUZ
iykTPDfWDccKSLM4sAz0uYxo5tJTVdt+1s7pmsAWDadZOxILcurz3zRqrpyjPcqU
+i4c1JBhWX1j0tYz6p5gs3GnhEmyrluALNmLEn6x9G671iDjmkyqK/WLefzyeo95
Yj3b2+EDq0wKwpL+yuiSvKEBHqbgmCwbQ8+D6Fp+hNEScgmt5efQ0iyQYDu/p4K7
0qOfVpzJYT+Sv+aVwXzclD/18W8rLfz+PV80Au87vseQW53BiuI4C5FqtFe1t3OY
kys2lVATeruwtlep+2Py1cyMgYyLjENc0h0sxmzhOid/KKCGWTeMB9SKX7rVI7Op
J9ErN58GeDyrhAiTur0F4cdvTmxZFGXaUvVoQCkWNo9iYngCeQ8TxCTGzVPRGMNv
hm4YYJAPw0P4H2+riAJFd0gqJ9B70bDaFep+/WnGOIBwpW3R7ElGzUpAGEWKSMTu
fgfSiQGzBBABCAAdFiEEmu4Bhq6CdRPO82CyQf9TSC2eROYFAlnRWtoACgkQQf9T
SC2eROYyOwv/WyC13Ae81ElGDefFyO8hu/d4xRx2wiWG4X6Dft6dyu5pv628l5Kd
ZUeowm/TdRL58yLpciJCn9NOGJ83Xwu3Fo5/1qhckuJyOcKuxaYoTahH8PQDtvCZ
Xb8lsvYGA7SOrp5jIihW72Bfz3Se5IibBUSyEKXE6Ro7qmR1drNIrTYldRo5YDMY
y4M00BEWcfi7z/KFWVdmwusxMz6/raW+sw9kPQbEsh7aTAORHqc0/vf7cdmIue+F
SU6GFcKojIkb/7AZlQzyfuaLogJ+80sBQVJDNSBYvmgNwH61BXAKs1divL4e/p52
eF7cJe1p66QrNHJXvBR4vxcS5novSVtO2exL0cAbF/e/f7+vBGViPzDi9fdKeyF0
KmW9KSncpiePQ1qkDvmHFfejV5bWa8hdrPc6X8PQ/AOPsNPw/8TBeXCTjg/nNZvC
pl9m12ISiIOhpURq1srpzw0JygxV+z+K/fkZuPo6QJFy9hSXUwZ1QzV8yLTyKz3i
aHrcwQRcK/jwuQINBFjwEUkBEAC1GrFLnjU4hKx3dEiEYQWgIAdKezZzLGYr7KgU
riNflGUEouWHg0OJoSD8rJ8IySeFKMUGHfPRci6i8FBgGGOUEBv4bwCvZeNSG5/S
mIiB5PDSSJhdqBGM3tGq72FWdRaLxDHyooGnjeMtwtSEGL7XmWtFd4daJL3GFnc0
arfCA0w+jBVGlUrVOy4VQ1hLgT6Yiu4mxdg3oun96fHaWkRh1YxGfWzlDx5963uS
LVUmZSatRcQHaNLEUeYYfFCUepKpz8bbQdPv0eF/A9mPn3zqZQypSdhDQPnTUCgv
omAEZhR4dghNXmXf/TmyBc+2oRXynv5HUfnVUr+ynjcz4k3HeYHjEst0Uue2Rz48
O9yP+VRSzOgl2HG8TfuD750UArk9WF06GFAnwWcv9AJalgLzQ1qJisurtyan0fkI
PWOgdQJwXgC7lGNZXE2xQMkcihWmDBdfK9YKpkyPhi4aSW8M///QP4WT4LFQe/nu
irhEQh8zuujA02HlzTbfdqlecpSkkMu2u+bgXHG/kC7wzMcCa+I7HyxKtXBd1MOC
+4c6PY0Am5QkwvLQVlAg7WWDW9kRpymN6xQc8hwlZChziXe3K2nv0rmM2+K1UVPB
Ub69wUHVUnXCCOAEfjiIFMloQa39+LLp2DamTrVaIkLDr21KXH90FXD+pydMlPBU
FkCXrQARAQABiQIfBBgBCAAJBQJY8BFJAhsMAAoJELvrTdOaxsypf7AP/0LMlW9v
WkTlFK8Q3QOmRadwwbrjwdPEBaoN2Ll4SwxQ2KBMBAZqyGnbV6f2JOvDep0S9Xwy
RvyUnTWlZb+Hz62xT/RJwhorRxBTEFAgkEbGK8jq5oSiixjOslKv8Qe1W6Qggh0+
dCbo2LVEDnlOmQlMpmf9k3fL8mDrOiYany/un1x7Fdtux6B7CtCIib4c84RrotuY
vFwS+bLKV2oHroHKqdt7iY5kZ4LBtrKuoo/2fYtIk+2jCmligOpsrI27zpS82SWp
wGspeOIukiNOtYA8kbjUxx4K7qGF5f35o+6wVOwPnWTjKtKm6BaltlUk7mgtXwYG
GRO44Nkw3sPE/9MAKhIJNm4Oq+2/av4ooE8dYXERUY0g5dR5PHX0O8lQBjDyGYoz
H/m1DDVoxOwkVbQP6alpie6XvM03mOWOklj20mlFAnyjLzdxw2m7vySOnXnvZq3s
SZUYTh0rsWmXt/ye1/E8aXtTqu9ZnuSAJh+8TLmGYppyjqr6PhENtZcOxUZyoKzc
A8ZrRzyuelHqiAWQBNDhOde+QYcBgYXaD1BQUO/9dWanUnP9Lbhu4zkxIC353m1e
xAYreSmvD5paqoL4jH5eJakdIasQTjyT8FJ/CI131fbxuVT9P2oaKt/7mjk0JKcz
/mmL8sP2wfO515ZBxq6+nuzOel+4IHFFqRFv
=XDDO
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -1,16 +1,19 @@
<template lang='pug'>
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}}
v-alert(v-if="error.statusCode === 404" type='error' :icon='mdiAlert') ¯\_()_/¯ {{error.message}}
v-alert(v-else type='error' :icon='mdiAlert') An error occurred: {{error.message}}
nuxt-link(to='/')
v-btn Back to home
</template>
<script>
import { mapState } from 'vuex'
import { mdiAlert } from '@mdi/js'
export default {
props: { error: { type: Object, default: () => ({ }) } },
data () {
return { mdiAlert }
},
head () {
return { title: `${this.settings.title} - Error` }
},

View file

@ -109,7 +109,7 @@
"list_description": "Si tens una web i vols encastar una llista d'activitats, pots fer servir el codi de sota"
},
"register": {
"description": "Els moviments socials necessitem organitzar-nos i auto-finançar-nos. <br/> Aquest és un regal per vosaltres, feu-lo servir només per usos no-comercials i òbviament per activitats antifeixistes, antisexistes, i antiracistes.\n <br/> Abans que puguis publicar, <strong> hem d'aprovar el teu compte </strong>, tingues en comtpe que <strong> darrere d'aquesta web hi ha persones </strong> de carn i ossos. Ens agradaria saber quin tipus d'activitats vols publicar, ens escrius dues línies explicant-ho?",
"description": "Els moviments socials necessitem organitzar-nos i auto-finançar-nos.\n<br/> Abans que puguis publicar, <strong> hem d'aprovar el teu compte </strong>, tingues en comtpe que <strong> darrere d'aquesta web hi ha persones </strong> de carn i ossos, així que escriviu dues línies per fer-nos saber quins esdeveniments voleu publicar.",
"error": "Error: ",
"complete": "El registre ha de ser confirmat.",
"first_user": "S'ha creat i activat un compte administrador"
@ -124,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 apareix el lloc el pots crear.",
"where_description": "On es farà? Si no està posat, escriu-ho i <b>prem Enter</b>.",
"confirmed": "S'ha confirmat l'activitat",
"not_found": "No s'ha trobat l'activitat",
"remove_confirmation": "Segur que vols esborrar l'activitat?",
@ -156,10 +156,10 @@
"ics": "ICS",
"import_ICS": "Importa des d'un ICS",
"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"
"updated": "S'ha actualitzat l'activitat",
"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)"
},
"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.",
@ -221,11 +221,12 @@
"add_instance": "Afegeix una instància",
"instance_block_confirm": "Segur que vols bloquejar la instància {instance}?",
"show_smtp_setup": "Configuració de correu",
"admin_email": "Correu d'admin",
"smtp_test_success": "S'ha enviat un correu de prova a {admin_email}, comprova que hagi arribat bé",
"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"
"widget": "Giny"
},
"auth": {
"not_confirmed": "Encara no s'ha confirmat…",
@ -270,7 +271,6 @@
},
"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"
}

View file

@ -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."
"content": "Hem rebut una soŀlicitud de registre. Hi respondrem tan aviat com ens sigui possible.<br/>Salut."
},
"confirm": {
"subject": "Ja pots publicar activitats",
@ -9,7 +9,7 @@
},
"user_confirm": {
"subject": "Ja pots publicar activitats",
"content": "Hola, que creat un compte a <a href='{{config.baseurl}}'>{{config.title}}</a>? Si és així, <a href='{{config.baseurl}}/user_confirm/{{user.recover_code}}'>confirma-ho i tria una contrasenya</a>, si us plau."
"content": "Hola, que has creat un compte a <a href='{{config.baseurl}}'>{{config.title}}</a>? Si és així, <a href='{{config.baseurl}}/user_confirm/{{user.recover_code}}'>confirma-ho i tria una contrasenya</a>, si us plau."
},
"recover": {
"subject": "Recupera la contrasenya",
@ -20,7 +20,7 @@
"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>"
"content": "Pots confirmar aquesta activitat a <a href='{{url}}'>la pàgina de confirmació</a>"
},
"test": {
"subject": "La configuració SMTP funciona",

View file

@ -18,5 +18,12 @@
"admin_register": {
"subject": "Izen-emate berria",
"content": "{{user.email}}-(e)k {{config.title}}-n izena ematea eskatu du: <br/><pre>{{user.description}}</pre><br/> Baieztatu ezazu horrela dela <a href='{{config.baseurl}}/admin'>hemen</a>."
},
"event_confirm": {
"content": "Ekitaldi hau <a href='{{url}}'>hemen</a> baieztatu dezakezu"
},
"test": {
"subject": "Zure SMTP konfigurazioa badabil",
"content": "Hau probako eposta bat da, mezu hau irakurtzen ari bazara konfigurazioa badabilela esan nahi du."
}
}

29
locales/email/gl.json Normal file
View file

@ -0,0 +1,29 @@
{
"register": {
"subject": "Recibida solicitude de rexistro",
"content": "Recibimos unha solicitude de rexistro. Confirmarémola o antes posible."
},
"recover": {
"subject": "Recuperación do contrasinal",
"content": "Ola, pediches recuperar o contrasinal en {{config.title}}. <a href='{{config.baseurl}}/recover/{{user.recover_code}}'>Preme aquí</a> para confirmalo."
},
"admin_register": {
"content": "{{user.email}} solicitou rexistrarse en {{config.title}}: <br/><pre>{{user.description}}</pre><br/> Confírmao <a href='{{config.baseurl}}/admin'>aquí</a>.",
"subject": "Nova conta"
},
"test": {
"subject": "A configuración SMTP é correcta",
"content": "Este é un email de proba, se estás lendo isto é que a configuración funciona."
},
"confirm": {
"subject": "Xa podes comezar a publicar eventos",
"content": "Ola, a túa conta en <a href='{{config.baseurl}}'>{{config.title}}</a> foi confirmada. Escríbenos a {{config.admin_email}} se precisas máis información."
},
"user_confirm": {
"content": "Ola, creouse a túa conta en <a href='{{config.baseurl}}'>{{config.title}}</a>. <a href='{{config.baseurl}}/user_confirm/{{user.recover_code}}'>Debes confirmala e elexir un contrasinal</a>.",
"subject": "Xa podes comezar a publicar eventos"
},
"event_confirm": {
"content": "Podes confirmar este evento <a href='{{url}}'>nesta páxina</a>"
}
}

View file

@ -21,5 +21,9 @@
},
"event_confirm": {
"content": "Puoi confermare questo evento premendo il tasto conferma in <a href='{{url}}'>questa pagina</a>"
},
"test": {
"subject": "La tua configurazione SMTP sta funzionando",
"content": "Questa è una email di test, se la stai leggendo significa che la tua configurazione sta funzionando."
}
}

View file

@ -109,7 +109,7 @@
"list_description": "If you have a website and want to show a list of events, use the following code"
},
"register": {
"description": "Social movements should organize and self-finance.<br/>\n<br/>Before you can publish, <strong> the account must be approved</strong>, consider that <strong> behind this site you will find real people, so write two lines to let us know what events you would like to publish.",
"description": "Social movements should organize and self-finance.<br/>\n<br/>Before you can publish, <strong> the account must be approved</strong>, consider that <strong> behind this site you will find real people</strong>, so write two lines to let us know what events you would like to publish.",
"error": "Error: ",
"complete": "Registration has to be confirmed.",
"first_user": "Administrator created"
@ -225,8 +225,8 @@
"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"
"admin_email": "Admin e-mail",
"widget": "Widget"
},
"auth": {
"not_confirmed": "Not confirmed yet…",
@ -272,6 +272,7 @@
"setup": {
"completed": "Setup completed",
"completed_description": "<p>You can now login with the following user:<br/><br/>User: <b>{email}</b><br/>Password: <b>{password}<b/></p>",
"copy_password_dialog": "Yes, you have to copy the password!",
"start": "Start"
}
}

View file

@ -1,11 +1,12 @@
export default {
it: 'Italiano',
ca: 'Català',
de: 'Deutsch',
en: 'English',
es: 'Español',
ca: 'Català',
pl: 'Polski',
eu: 'Euskara',
nb: 'Norwegian Bokmål',
fr: 'Francais',
de: 'Deutsch'
gl: 'Galego',
it: 'Italiano',
nb: 'Norwegian Bokmål',
pl: 'Polski',
}

View file

@ -1,6 +1,6 @@
{
"common": {
"add_event": "Ekitaldia sortu",
"add_event": "Sortu ekitaldia",
"next": "Jarraitu",
"export": "Esportatu",
"send": "Bidali",
@ -10,58 +10,58 @@
"what": "Zer",
"media": "Media",
"login": "Saioa hasi",
"email": "Email",
"email": "Eposta",
"password": "Pasahitza",
"register": "Izena eman",
"description": "Deskribapena",
"remove": "Ezabatu",
"hide": "Ezkutatu",
"search": "Bilatu",
"edit": "Aldatu",
"edit": "Editatu",
"info": "Informazioa",
"confirm": "Baieztatu",
"admin": "Kudeatu",
"admin": "Administratu",
"users": "Erabiltzaileak",
"events": "Ekitaldiak",
"places": "Lekuak",
"settings": "Ezarpenak",
"settings": "Aukerak",
"actions": "Ekintzak",
"deactivate": "Desaktibatu",
"remove_admin": "Administratzailea kanporatu",
"remove_admin": "Kendu administratzaile baimena",
"activate": "Aktibatu",
"save": "Gorde",
"preview": "Aurrebista",
"logout": "Saioa amaitu",
"logout": "Itxi saioa",
"share": "Partekatu",
"name": "Izena",
"associate": "Elkartu",
"edit_event": "Ekitaldia aldatu",
"edit_event": "Editatu ekitaldia",
"related": "Erlazionatuta",
"add": "Gehitu",
"logout_ok": "Saioa ondo itxi da",
"copy": "Kopiatu",
"recover_password": "Pasahitza berreskuratu",
"recover_password": "Berreskuratu pasahitza",
"new_password": "Pasahitz berria",
"new_user": "Erabiltzaile berria",
"ok": "Ados",
"cancel": "Ezeztatzea",
"cancel": "Utzi",
"enable": "Gaitu",
"disable": "Desgaitu",
"me": "Zu",
"password_updated": "Pasahitza eguneratuta.",
"activate_user": "Egiaztatuta",
"password_updated": "Pasahitza aldatu da.",
"activate_user": "Baieztatuta",
"displayname": "Erakutsitako izena",
"federation": "Federazioa",
"set_password": "Pasahitza ezarri",
"copy_link": "Lotura kopiatu",
"send_via_mail": "Posta elektronikoa bidali",
"add_to_calendar": "Egutegira gehitu",
"set_password": "Ezarri pasahitza",
"copy_link": "Kopiatu esteka",
"send_via_mail": "Bidali eposta",
"add_to_calendar": "Gehitu egutegira",
"instances": "Instantziak",
"copied": "Kopiatuta",
"embed": "Txertatuta",
"embed_title": "Txertatu ekitaldi hau zure web-gunean",
"embed_help": "Hurrengo kodea zure web-orrian kopiatuz gero ekitaldia txertatuko da hemen ikusten den bezala",
"feed": "RSS Jarioa",
"embed": "Kapsulatu",
"embed_title": "Kapsulatu ekitaldi hau zure webgunean",
"embed_help": "Kode hau zure webgunean kopiatu eta ekitaldia bertan honela ikusiko da",
"feed": "RSS jarioa",
"feed_url_copied": "Erabili RSS jarioaren esteka zure RSS jario irakurgailuan",
"follow_me_title": "Fedibertsoko eguneraketak jarraitu",
"follow": "Jarraitu",
@ -78,65 +78,65 @@
"fediverse": "Fedibertsoa",
"skip": "Saltatu",
"delete": "Ezabatu",
"announcements": "Iragarkiak",
"url": "URL esteka",
"announcements": "Iragarpenak",
"url": "URLa",
"place": "Lekua",
"label": "Etiketa",
"max_events": "Max zenbakidun gertaerak",
"label": "Izena",
"max_events": "Gehienezko ekitaldi kopurua",
"import": "Inportatu",
"reset": "Zeroan jarri",
"theme": "Gai",
"tags": "Tags"
"reset": "Berrezarri",
"theme": "Itxura",
"tags": "Etiketak"
},
"login": {
"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?",
"description": "Saioa hasten baduzu ekitaldi berriak sortu ahal izango dituzu.",
"check_email": "Begiratu zure epostaren sarrera-ontzia eta zabor karpeta.",
"not_registered": "Ez duzu izenik eman?",
"forgot_password": "Pasahitza ahaztu duzu?",
"error": "Ezin da saioa hasi, egiaztatu zure datuok.",
"insert_email": "Sartu zure helbide elektronikoa",
"error": "Ezin izan da saioa hasi. Egiaztatu datuak.",
"insert_email": "Sartu zure eposta helbidea",
"ok": "Saioa hasi duzu"
},
"recover": {
"not_valid_code": "Mmmmm zerbaitek huts egin du..."
"not_valid_code": "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.",
"insert_your_address": "Sartu zure helbide elektronikoa",
"intro": "Kapitalismoaren plataformek edozer egingo dute erabiltzaileak eta haien datuak gordetzeko. Guk aldiz, informazioa, pertsonak bezala, askea izan behar duela uste dugu. Horregatik hemengo ekitaldien informazio eguneratua jaso dezakezu webgune honetatik pasatu beharrik ere izan gabe.",
"email_description": "Zure intereseko ekitaldiak epostaz jaso ditzakezu.",
"insert_your_address": "Sartu zure eposta helbidea",
"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"
"ical_description": "Ordenagailuak eta telefonoak maiz egutegiak inportatzeko eta kudeatzeko aplikazioekin datoz.",
"list_description": "Webgune bat baduzu eta ekitaldien zerrenda erakutsi nahi baduzu, ondorengo kodea erabili"
},
"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.",
"error": "Hutsa: ",
"complete": "Izen-ematea baieztatu behar dute.",
"error": "Errorea: ",
"complete": "Izen-ematea baieztatua izan behar da.",
"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/>\nDena den, ahalik eta azkarren erantzuten saiatuko gara. ",
"anon": "Anonimoa",
"anon_description": "Ekitaldi bat sor dezakezu saioa hasi edo izena eman gabe, baina norbaitek berau irakurri eta\ngune honetarako egokia dela baieztatu arte itxaron beharko duzu. Ez da posible izango ekitaldia aldatzerik.<br/><br/>\nHorren ordez <a href='/login'>saioa hasi</a> edo <a href='/register'>izena eman</a> dezakezu. Bestela, jarraitu aurrera eta ahalik azkarren erantzuten saiatuko gara. ",
"same_day": "egun berean",
"what_description": "Ekitaldiaren izena",
"description_description": "Ekitaldiaren azalpena",
"tag_description": "Etiketak (tag)...",
"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>.",
"what_description": "Izenburua",
"description_description": "Deskripzioa",
"tag_description": "Etiketa",
"media_description": "Irudi bat gehi dezakezu (hautazkoa)",
"added": "Ekitaldia gehitu da",
"added_anon": "Ekitaldia gehitu da, baina baieztatzeko zain dago.",
"where_description": "Non da ekitaldia? Agertzen ez bada zuk sor dezakezu.",
"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?\nIragan diren ekitaldiak mantenduko dira, baina ez da ekitaldi berririk sortuko.",
"remove_recurrent_confirmation": "Ziur zaude ekitaldi errepikari hau ezabatu nahi duzula?\nIragandako 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": "Egun gehiagotan",
"normal": "Egunekoa",
"normal_description": "Eguna aukeratu.",
"show_recurrent": "ekitaldi errepikariak",
"show_past": "iragandako ekitaldiak ere",
"recurrent_description": "Aukera maiztasuna eta hautatu egunak",
"multidate_description": "Jaialdi bat da? Aukeratu noiz hasten eta amaitzen den",
"multidate": "Egun gehiagokoa",
"normal": "Normala",
"normal_description": "Aukeratu eguna.",
"recurrent_1w_days": "{days}(e)ro",
"recurrent_2w_days": "Bi {days}(e)z behin",
"recurrent_1m_days": "|Hilabetero {days}etan|Hilabetero {days}etan",
@ -148,118 +148,129 @@
"each_month": "Hilero",
"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)",
"import_description": "Beste plataforma eta adibide batzuetako gertaerak formatu estandarren bidez inportatu ditzakezu (ics eta h-event)",
"image_too_big": "Irudia ezin da 4 MB baino handiagoa izan",
"interact_with_me": "Jarrai nazazu",
"follow_me_description": "{title}(e)n argitaratutako ekitaldien berri izateko aukeren artean,\nfedibertsoko <u>{account}</u> kontua jarraitzea daukazu. Horretarako, Mastodon erabil dezakezu eta bertatik ekitaldi bati baliabideak gehitu.<br/><br/>\nMastodon eta fedibertsoa zer diren ez badakizu <a href='https://eu.wikipedia.org/wiki/Fedibertso'>artikulu hau</a> irakurtzea gomendatzen dizugu.<br/><br/>Sartu zure instantzia behean (adibidez, mastodon.eus edo mastodon.jalgi.eus)",
"import_description": "Beste plataforma eta instantzietatik inportatu ditzakezu ekitaldiak formatu estandarrak erabiliz (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"
"import_ICS": "ICS-tik inportatu",
"import_URL": "URL-tik inportatu",
"interact_with_me_at": "Mintzatu nirekin fedibertsoko leku honetan:",
"only_future": "datozen ekitaldiak bakarrik",
"edit_recurrent": "Editatu ekitaldi errepikaria:",
"updated": "Ekitaldia eguneratu da",
"saved": "Ekitaldia gorde da"
},
"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 {user} ezabatu nahi duzula?",
"place_description": "Helbidea oker badago, alda dezakezu.<br/>Leku honekin lotutako iraganeko eta etorkizuneko ekitaldien helbidea aldatuko da.",
"event_confirm_description": "Erabiltzaile anonimoek sortutako ekitaldiak hemen baieztatu ditzakezu",
"delete_user": "Ezabatu",
"remove_admin": "Kendu administratzaile baimena",
"delete_user_confirm": "Ziur al 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_anon_event": "Anonimoek ekitaldiak sortzea ahalbidetu (baieztatu ondoren)?",
"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": "Federazioa gaitu",
"enable_federation_help": "Instantzia hau fedibertsotik jarraitzea posible izango da",
"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..",
"instance_timezone_description": "Gancio gune bateko ekitaldiak biltzeko diseinatuta dago, adibidez hiri batekoak. Hemengo ekitaldi guztiak aukeratzen duzun ordu-eremuan erakutsiko dira.",
"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",
"enable_resources_help": "Fedibertsotik ekitaldiei baliabideak gehitzea ahalbidetzen du",
"hide_boost_bookmark": "Bultzadak eta laster-markak ezkutatzen ditu",
"hide_boost_bookmark_help": "Fedibertsotik datozen bultzada eta laster-marka kopuruaren ikonotxoak ezkutatzen ditu",
"block": "Blokeatu",
"unblock": "Desblokeatu",
"user_add_help": "Mezu elektroniko bat bidaliko diogu erabiltzaile berriari harpidetza baieztatzeko eta pasahitz bat aukeratzeko argibideekin",
"user_add_help": "Eposta bat bidaliko diogu erabiltzaile berriari harpidetza baieztatzeko eta pasahitz bat aukeratzeko argibideekin",
"resources": "Baliabideak",
"hide_resource": "Baliabidea ezkutatu",
"show_resource": "Baliabidea erakutsi",
"delete_resource": "Baliabidea ezabatu",
"hide_resource": "Ezkutatu baliabidea",
"show_resource": "Erakutsi baliabidea",
"delete_resource": "Ezabatu baliabidea",
"delete_resource_confirm": "Ziur zaude baliabide hau ezabatu nahi duzula?",
"block_user": "Erabiltzailea blokeatu",
"block_user": "Blokeatu erabiltzailea",
"user_blocked": "{user} erabiltzailea blokeatuta dago",
"filter_instances": "Instantziak iragazi",
"filter_users": "Erabiltzaileak iragazi",
"filter_instances": "Iragazi instantziak",
"filter_users": "Iragazi erabiltzaileak",
"instance_name": "Instantziaren izena",
"favicon": "Iruditxoa",
"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.",
"description_description": "Orriburuan agertuko da, izenburuarekin batera",
"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?",
"favicon": "Logoa",
"user_block_confirm": "Ziur al zaude {user} blokeatu nahi duzula?",
"delete_announcement_confirm": "Ziur al zaude iragarpena ezabatu nahi duzula?",
"announcement_remove_ok": "Iragarpena ezabatu da",
"announcement_description": "Atal honetan iragarpenak txertatu ditzakezu hasiera-orrialdean ager daitezen",
"instance_locale": "Hizkuntza lehenetsia",
"instance_locale_description": "Orrialdeak erabiltzailearen gogoko hizkuntzan bistaratzen dira. Batzuetan mezuak jende guztiari hizkuntza berean bistaratu beharra dago (adibidez, ActivityPub bidez argitaratzen dugunean edo eposta batzuetan). Kasu hauetan goian hautatutako hizkuntza erabiliko dugu.",
"instance_place": "Instantziaren kokalekua adierazten du",
"title_description": "Orrialdearen izenburuan, ICS jarioetan eta RSS-ak esportatzeko epostaren gaian erabiltzen da.",
"description_description": "Goiburuan agertzen da izenburutik gertu",
"instance_name_help": "Jarraitu beharreko ActivityPub kontua",
"enable_trusted_instances": "Gaitu instantzia adiskidetsuak",
"trusted_instances_help": "Kideak diren instantzien zerrenda goiburuan erakutsiko da",
"add_trusted_instance": "Gehitu kide den instantzia bat",
"instance_place_help": "Beste instantzietan agertuko den izena",
"delete_trusted_instance_confirm": "Ziur al zaude instantzia hau menuko zerrendatik kendu nahi duzula?",
"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?"
"edit_place": "Editatu lekua",
"delete_footer_link_confirm": "Ziur al zaude esteka hau kendu nahi duzula?",
"footer_links": "Oineko estekak",
"add_link": "Gehitu esteka",
"is_dark": "Itxura iluna",
"instance_block_confirm": "Ziur al zaude {instance} instantzia blokeatu nahi duzula?",
"add_instance": "Gehitu instantzia",
"disable_user_confirm": "Ziur zaude {user} deskonektatu nahi duzula?",
"show_smtp_setup": "Eposta ezarpenak",
"smtp_test_button": "Bidali probako eposta bat",
"smtp_test_success": "Probako eposta bidali da {admin_email}-(e)ra, begiratu zure sarrera-ontzia",
"admin_email": "Administratzailearen eposta",
"smtp_hostname": "SMTP hostname",
"smtp_description": "<ul><li>Administratzaileak eposta bat jaso beharko luke anonimo batek ekitaldi bat gehitzen duenean (gaituta badago).</li><li>Administratzaileak eposta bat jaso beharko luke izena emateko eskari bakoitzeko (gaituta badago).</li><li>Erabiltzaileak eposta bat jaso beharko luke izena emateko eskariarekin.</li><li>Erabiltzaileak eposta bat jaso beharko luke izen ematea baieztatzean.</li><li>Erabiltzaileak eposta bat jaso beharko luke administratzaileak zuzenean izena emanez gero.</li><li>Erabiltzaileek eposta bat jaso beharko lukete pasahitza ahazten dutenean.</li></ul>"
},
"auth": {
"not_confirmed": "Oraindik baieztatu gabe dago…",
"fail": "Saioa hasteak huts egin du! Ziur zaude datuok ondo daudela?"
"fail": "Ezin izan da saioa hasi. Ziur al zaude pasahitza zuzena dela?"
},
"settings": {
"update_confirm": "Aldaketak gorde nahi duzu?",
"change_password": "Pasahitza aldatu",
"password_updated": "Pasahitza eguneratu da.",
"change_password": "Aldatu pasahitza",
"password_updated": "Pasahitza aldatu 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 sakatuz 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": "Ezizen hau dagoeneko hartuta dago.",
"email_taken": "Eposta hau dageneko hartuta dago."
},
"confirm": {
"title": "Erabiltzaile-baieztapena",
"not_valid": "Mmmmm zerbaitek huts egiten du.",
"not_valid": "Zerbaitek huts egiten du.",
"valid": "Zure kontua baieztatua izan da, orain <a href=\"/login\">saioa hasi</a> dezakezu"
},
"ordinal": {
"1": "lehen",
"2": "bigarren",
"3": "hirugarren",
"4": "laugarrena",
"5": "bostgarrena",
"-1": "azkena"
"4": "laugarren",
"5": "bosgarren",
"-1": "azken"
},
"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",
"authorization_request": "<code>{app}</code> aplikazioak ondorengo baimena eskatu du <code>{instance_name}</code>(e)n:",
"redirected_to": "Baieztatu ondoren hona birbideratua izango zara: <code>{url}</code>",
"scopes": {
"event:write": "Zure ekitaldiak sortu eta aldatu"
"event:write": "Sortu eta editatu zure ekitaldiak"
}
},
"validators": {
"email": "Sar ezazu posta elektroniko baliozko bat",
"email": "Sartu baliozko eposta bat",
"required": "{fieldName} beharrezkoa da"
},
"setup": {
"start": "Hasi",
"completed": "Instalazioa bukatu da",
"completed_description": "<p>Erabiltzaile honekin saioa has dezakezu orain:<br/><br/>Erabiltzailea: <b>{email}</b><br/>Pasahitza: <b>{password}<b/></p>"
}
}

View file

@ -136,7 +136,7 @@
"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.",
"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</strong>, à qui vous pouvez écrire en deux lignes pour exprimer les évènements que vous souhaiteriez publier.",
"first_user": "Administrateur créé",
"complete": "L'inscription doit être confirmée.",
"error": "Erreur : "
@ -213,7 +213,8 @@
"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"
"admin_email": "E-mail de l'administrateur",
"widget": "Vignette active"
},
"oauth": {
"scopes": {

277
locales/gl.json Normal file
View file

@ -0,0 +1,277 @@
{
"common": {
"export": "Exportar",
"send": "Enviar",
"address": "Enderezo",
"when": "Cando",
"what": "Que",
"media": "Multimedia",
"login": "Acceder",
"email": "Email",
"password": "Contrasinal",
"register": "Rexistro",
"description": "Descrición",
"remove": "Eliminar",
"hide": "Agochar",
"search": "Buscar",
"edit": "Editar",
"events": "Eventos",
"enable": "Activar",
"disable": "Desactivar",
"me": "Ti",
"password_updated": "Cambiou o contrasinal.",
"resources": "Recursos",
"activate_user": "Confirmado",
"instances": "Instancias",
"copied": "Copiado",
"embed_help": "Copia o seguinte código ao teu sitio web e o evento será mostrado como aquí",
"feed_url_copied": "Abrir o URL da fonte copiado no teu lector de RSS",
"event": "Evento",
"pause": "Pausa",
"start": "Inicio",
"fediverse": "Fediverso",
"announcements": "Anuncios",
"reset": "Restablecer",
"label": "Etiqueta",
"confirm": "Confirmar",
"admin": "Admin",
"users": "Usuarias",
"displayname": "Nome público",
"activate": "Activar",
"actions": "Accións",
"title": "Título",
"n_resources": "sen recurso|un recurso|{n} recursos",
"federation": "Federación",
"place": "Lugar",
"delete": "Eliminar",
"info": "Info",
"add_event": "Engadir evento",
"associate": "Asociar",
"next": "Seguinte",
"where": "Onde",
"places": "Lugares",
"settings": "Opcións",
"deactivate": "Apagar",
"remove_admin": "Eliminar admin",
"logout_ok": "Non entraches",
"new_user": "Nova usuaria",
"ok": "Ok",
"cancel": "Cancelar",
"save": "Gardar",
"preview": "Vista previa",
"logout": "Saír",
"share": "Compartir",
"name": "Nome",
"edit_event": "Editar evento",
"copy": "Copiar",
"new_password": "Novo contrasinal",
"send_via_mail": "Enviar email",
"embed": "Incrustado",
"embed_title": "Inclúe este evento no teu sitio web",
"related": "Relacionado",
"add": "Engadir",
"recover_password": "Recuperar contrasinal",
"moderation": "Moderación",
"set_password": "Establecer contrasinal",
"copy_link": "Copiar ligazón",
"add_to_calendar": "Engadir ao calendario",
"follow_me_title": "Seguir actualizacións desde o fediverso",
"follow": "Seguir",
"user": "Usuaria",
"filter": "Filtro",
"skip": "Omitir",
"max_events": "N. máx. eventos",
"feed": "Fonte RSS",
"authorize": "Autorizar",
"url": "URL",
"theme": "Decorado",
"import": "Importar",
"tags": "Cancelos"
},
"recover": {
"not_valid_code": "Algo fallou."
},
"export": {
"email_description": "Podes ter eventos que che interesen a través do email.",
"insert_your_address": "Escribe o enderezo de email",
"ical_description": "As computadoras e teléfonos traen normalmente instalada unha aplicación de calendario que pode importar un calendario remoto.",
"list_description": "Se tes un sitio web e queres mostrar unha lista de eventos, usa o seguinte código",
"intro": "Ao contrario que nas plataformas asociais que fan todo o posible para obter e gardar datos das persoas usuarias, cremos que esta información, como as persoas, deben ser libres. Por isto, podes recibir actualizacións dos eventos que elixas, sen precisar vir a esta web.",
"feed_description": "Para ter actualizacións usando unha computadora ou un teléfono intelixente sen ter que vir a esta web, usa fontes RSS. </p>\n\n<p> Coas fontes RSS usas unha app especial para recibir información das webs que che interesan. É un bo xeito de seguir moitas webs de xeito rápido, se ter que crear unha conta ou outras complicacións. </p>\n\n<li> Se usas Android, recomendámosche <a href=\"https://f-droid.org/en/packages/net.frju.flym/\">Flym</a> ou Feeder </li>\n<li> Para iPhone / iPad podes usar <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\"> Feed4U </a> </li>\n<li> Na computadora recomendamos Feedbro, que se pode instalar en <a href=\"https://addons.mozilla.org/en-GB/firefox/addon/feedbroreader/\"> Firefox </a> ou en <a href=\"https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa\"> Chrome </a>. </li>\n<br/>\nSe engades esta ligazón ao teu lector de RSS poderás recibir as actualizacións."
},
"register": {
"complete": "É preciso confirmar o rexistro.",
"first_user": "Creada Admin",
"description": "Os movementos sociais deberían organizarse e auto-financiarse.<br/>\n<br/>Antes de poder publicar, <strong> a conta debe ser aprobada</strong>, ten en conta que <strong> detrás desta web hai persoas reais</strong>, así que escribe un par de liñas para coñecer o tipo de eventos que che gustaría publicar.",
"error": "Erro: "
},
"event": {
"media_description": "Podes engadir un panfleto (optativo)",
"added": "Evento engadido",
"saved": "Evento gardado",
"updated": "Evento actualizado",
"where_description": "Onde será o evento? Se non existe podes crealo.",
"not_found": "Non atopamos o evento",
"show_recurrent": "eventos recurrentes",
"show_past": "tamén eventos previos",
"only_future": "só eventos futuros",
"recurrent_2w_days": "Cada {days} días",
"recurrent_2m_ordinal": "|O {n} {days} en meses alternos|O {n} {days} en meses alternos",
"recurrent_2m_days": "|O {days} cada dous meses|O {days} cada dous meses",
"each_month": "Cada mes",
"due": "ata",
"from": "Desde",
"interact_with_me_at": "Contacta comigo no fediverso en",
"interact_with_me": "Sígueme",
"remove_recurrent_confirmation": "Tes a certeza de querer eliminar este evento recurrente?\nOs eventos pasados permanecerán, pero non se crearán novos eventos.",
"import_URL": "Importar desde URL",
"anon": "Anon",
"anon_description": "Podes engadir un evento sen rexistrarte nen acceder, pero deberás agardar a que alguén o lea e\nconfirme que é un evento axeitado. Non será posible modificalo.<br/><br/>\nPodes tamén <a href='/login'>acceder</a> ou <a href='/register'>crear unha conta</a>. Mais podes continuar e agardar a que se comprobe. ",
"same_day": "no mesmo día",
"tag_description": "Cancelo",
"description_description": "Descrición",
"what_description": "Título",
"added_anon": "Evento engadido, mais ten que ser confirmado.",
"confirmed": "Evento confirmado",
"remove_confirmation": "Tes a certeza de querer eliminar este evento?",
"multidate_description": "É un festival? Elixe cando comeza e remata",
"import_ICS": "Importar desde ICS",
"edit_recurrent": "Editar evento recurrente:",
"recurrent_description": "Elixe a frecuencia e os días",
"multidate": "Máis días",
"normal": "Normal",
"each_week": "Cada semana",
"recurrent": "Recurrente",
"normal_description": "Elixe o día.",
"recurrent_1w_days": "Cada {days}",
"recurrent_1m_ordinal": "O {n} {days} de cada mes",
"image_too_big": "A imaxe non pode superar os 4 MB",
"recurrent_1m_days": "|O {days} de cada mes|{days} de cada mes",
"each_2w": "Cada dúas semanas",
"follow_me_description": "Un dos xeitos de recibir actualizacións dos eventos que se publican aquí en {title},\né seguindo a conta <u>{account}</u> no fediverso, por exemplo a través de Mastodon, e posiblemente tamén engadir recursos para un evento desde alí.<br/><br/>\nSe nunco escoitaches falar de Mastodon e o fediverso recomendámosche ler <a href='https://www.savjee.be/videos/simply-explained/mastodon-and-fediverse-explained/'>este artigo</a>.<br/><br/>Escribe aquí a túa instancia (ex. mastodon.social)",
"ics": "ICS",
"import_description": "Podes importar eventos desde outras plataformas e outras instancias usando formatos estándar (ics e h-event)"
},
"admin": {
"place_description": "Se escribiches mal o lugar ou enderezo, podes cambialo.<br/>Cambiará o enderezo de tódolos eventos actuais e pasados asociados a este lugar.",
"disable_user_confirm": "Tes a certeza de querer desactivar a {user}?",
"federation": "Federación / ActivityPub",
"enable_federation": "Activar a federación",
"add_instance": "Engadir instancia",
"select_instance_timezone": "Zona horaria",
"enable_resources_help": "Permite engadir recursos ao evento desde o fediverso",
"user_add_help": "Vaise enviar un email á nova usuaria con instruccións para confirmar a subscrición e elexir un contrasinal",
"instance_name": "Nome da instancia",
"filter_users": "Filtrar usuarias",
"title_description": "Utilízase no título da páxina, no asunto do email e para as fontes RSS e ICS exportadas.",
"description_description": "Aparece na cabeceira a carón do título",
"instance_name_help": "Conta ActivityPub a seguir",
"add_trusted_instance": "Engade unha instancia amiga",
"edit_place": "Editar lugar",
"smtp_test_success": "Enviado un email de proba a {admin_email}, comproba a caixa de correo",
"smtp_test_button": "Enviar un email de proba",
"admin_email": "Email de Admin",
"widget": "Trebello",
"delete_user_confirm": "Tes a certeza de querer eliminar a {user}?",
"enable_resources": "Activar recursos",
"hide_boost_bookmark_help": "Agocha as pequenas iconas que indican o número de promocións e marcacións que chegan desde o fediverso",
"show_resource": "Mostrar recurso",
"filter_instances": "Filtrar instancias",
"resources": "Recursos",
"allow_registration_description": "Permitir o rexistro libre?",
"allow_anon_event": "Permitir eventos anónimos (haberá que confirmalos)?",
"event_confirm_description": "Aquí podes confirmar os eventos engadidos por usuarias anónimas",
"remove_admin": "Eliminar admin",
"delete_user": "Eliminar",
"user_create_ok": "Usuaria creada",
"hide_boost_bookmark": "Agochar promoción/marcador",
"delete_resource": "Eliminar recurso",
"delete_resource_confirm": "Tes a certeza de querer eliminar este recurso?",
"favicon": "Logotipo",
"user_remove_ok": "Usuaria eliminada",
"allow_recurrent_event": "Permitir eventos recurrentes",
"recurrent_event_visible": "Mostrar por defecto os eventos recurrentes",
"enable_federation_help": "Vai ser posible seguir esta instancia desde o fediverso",
"block": "Bloquear",
"block_user": "Bloquear usuaria",
"user_blocked": "Usuaria {user} bloqueada",
"unblock": "Desbloquear",
"hide_resource": "Agochar recurso",
"announcement_remove_ok": "Anuncio eliminado",
"instance_timezone_description": "Gancio está deseñada para recoller os eventos dun lugar concreto, como unha cidade. Tódolos eventos neste lugar mostraranse na zona horaria elexida para el.",
"user_block_confirm": "Tes a certeza de querer bloquear a usuaria {user}?",
"instance_block_confirm": "Tes a certeza de querer bloquear a instancia {instance}?",
"announcement_description": "Nesta sección podes incluír anuncios que aparecerán na páxina de inicio",
"instance_locale": "Idioma por defecto",
"delete_announcement_confirm": "Tes a certeza de querer eliminar o anuncio?",
"instance_locale_description": "Idioma preferido para as páxinas. A veces as mensaxes teñen que mostrarse no mesmo idioma para tódalas persoas (por exemplo cando publicas vía ActivityPub ou cando envías os emails). Nestos casos usarase o idioma elexido aquí arriba.",
"enable_trusted_instances": "Activar instancias amigas",
"trusted_instances_help": "A lista das instancias amigas será mostrada na cabeceira",
"delete_trusted_instance_confirm": "Tes a certeza de querer eliminar este elemento do menú de instancias amigas?",
"instance_place_help": "A etiqueta a mostrar nas instancias de outras",
"add_link": "Engadir ligazón",
"instance_place": "Lugar de referencia para esta instancia",
"is_dark": "Decorado escuro",
"footer_links": "Ligazóns do rodapé",
"delete_footer_link_confirm": "Tes certeza de eliminar a ligazón?",
"show_smtp_setup": "Axustes do email",
"smtp_hostname": "Servidor SMTP",
"new_announcement": "Novo anuncio",
"smtp_description": "<ul><li>Admin debería recibir un email cando se engade un evento anónimo (se está activo)</li><li>Admin debería recibir un email coas solicitudes de rexistro (se activo).</li><li>A usuaria debería recibir un email coa solicitude de rexistro.</li><li>A usuaria debería recibir un email confirmando o rexistro.</li><li>A usuaria debería recibir un email de confirmación cando fose subscrita directamente por Admin</li><li>As usuarias deberían recibir un email para restablecer o contrasinal se o esquecen</li></ul>"
},
"auth": {
"not_confirmed": "Aínda non foi confirmado…",
"fail": "Non accedeches. Tes certeza acerca do contrasinal?"
},
"settings": {
"change_password": "Cambia o contrasinal",
"danger_section": "Sección perigosa",
"remove_account": "Ao premer o seguinte botón vas eliminar túa conta de usuaria. Os eventos que publicaches permanecerán.",
"password_updated": "Contrasinal cambiado.",
"remove_account_confirm": "Vas eliminar permanentemente a túa conta",
"update_confirm": "Queres gardar as modificacións?"
},
"error": {
"nick_taken": "Este alcume xa está a ser utilizado.",
"email_taken": "Este email xa está a ser utilizado."
},
"confirm": {
"title": "Confirmación da usuaria",
"not_valid": "Algo fallou.",
"valid": "A túa conta foi confirmada, xa podes <a href=\"/login\">acceder</a>"
},
"ordinal": {
"1": "primeiro",
"3": "terceiro",
"4": "cuarto",
"5": "quinto",
"-1": "último",
"2": "segundo"
},
"validators": {
"required": "{fieldName} é requerido",
"email": "Escribe un email válido"
},
"about": "\n <p><a href='https://gancio.org'>Gancio</a> é unha axenda compartida para comunidades locais.</p>\n ",
"oauth": {
"authorization_request": "A aplicación <code>{app}</code> solicita a seguinte autorización para <code>{intance_name}</code>:",
"redirected_to": "Após a confirmación vas ser enviada a <code>{url}</code>",
"scopes": {
"event:write": "Engade e edita os teus eventos"
}
},
"setup": {
"completed": "Configuración completada",
"completed_description": "<p>Xa podes acceder con estas credenciais:<br/><br/>Identificador: <b>{email}</b><br/>Contrasinal: <b>{password}</b></p>",
"start": "Comezar"
},
"login": {
"forgot_password": "Esqueceches o contrasinal?",
"check_email": "Comproba a caixa de correo e a de spam.",
"insert_email": "Escribe o enderezo de email",
"ok": "Entraches en",
"description": "Ao acceder poderás publicar novos eventos.",
"not_registered": "Non te rexistraches?",
"error": "Non entraches. Comproba as credenciais."
}
}

View file

@ -7,5 +7,6 @@ module.exports = {
eu: 'Euskara',
nb: 'Norwegian Bokmål',
fr: 'Francais',
de: 'Deutsch'
de: 'Deutsch',
gl: 'Galego',
}

View file

@ -223,7 +223,13 @@
"delete_footer_link_confirm": "Vuoi eliminare questo collegamento?",
"edit_place": "Modifica luogo",
"new_announcement": "Nuovo annuncio",
"show_smtp_setup": "Impostazioni email"
"show_smtp_setup": "Impostazioni email",
"widget": "Widget",
"smtp_description": "<ul><li>L'amministratore riceve una email quando viene aggiunto un evento anonimo (se abilitati).</li><li>L'amministratore riceve le mail di richiesta di registrazione (se abilitata).</li><li>L persone ricevono una mail di conferma della registrazione richiesta.</li><li>Le persone ricevono le email della registrazione confermata.</li><li>Le persone ricevono una mail di avviso quando vengono registrate direttamente da admin.</li><li>Le persone ricevono la mail per modificare la password quando l'hanno dimenticata</li></ul>",
"smtp_hostname": "SMTP Hostname",
"smtp_test_success": "Una mail di test è stata inviata all'indirizzo {admin_email}, controlla la tua casella di posta",
"smtp_test_button": "Invia una mail di prova",
"admin_email": "E-mail dell'admin"
},
"auth": {
"not_confirmed": "Non ancora confermato…",
@ -269,6 +275,7 @@
"setup": {
"completed": "Setup completato",
"completed_description": "<p>Puoi entrare con le seguenti credenziali:<br/><br/>Utente: <b>{email}</b><br/>Password: <b>{password}<b/></p>",
"copy_password_dialog": "Sì, devi copiare la password!",
"start": "Inizia"
}
}

View file

@ -1,9 +1,14 @@
export default function ({ req, redirect, route }) {
export default async function ({ $config, req, res, redirect, route, error }) {
if (process.server) {
if (req.firstrun && route.path !== '/setup') {
return redirect('/setup')
if (res.locals.status === 'SETUP' && route.path !== '/setup/0') {
return redirect('/setup/0')
}
if (!req.firstrun && route.path === '/setup') {
if (res.locals.status === 'DBCONF' && route.path !== '/setup/1') {
return redirect('/setup/1')
}
if (res.locals.status === 'READY' && route.path.startsWith('/setup')) {
return redirect('/')
}
}

View file

@ -1,5 +1,7 @@
const config = require('./server/config.js')
const minifyTheme = require('minify-css-string').default
const isDev = (process.env.NODE_ENV !== 'production')
module.exports = {
telemetry: false,
modern: (process.env.NODE_ENV === 'production') && 'client',
@ -11,31 +13,26 @@ module.exports = {
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
],
script: [{ src: '/gancio-events.es.js' }],
link: [{ rel: 'icon', type: 'image/png', href: '/logo.png' }]
link: [{ rel: 'icon', type: 'image/png', href: '/logo.png' }],
link: [{ rel: 'preload', type: 'image/png', href: '/logo.png', as: 'image' }],
script: [{ src: '/gancio-events.es.js', async: true, body: true }],
},
dev: (process.env.NODE_ENV !== 'production'),
dev: isDev,
server: config.server,
vue: {
config: {
ignoredElements: ['gancio-events']
ignoredElements: ['gancio-events', 'gancio-event']
}
},
css: ['./assets/style.less'],
/*
** Customize the progress-bar component
*/
loading: '~/components/Loading.vue',
/*
** Global CSS
*/
css: [
'vuetify/dist/vuetify.min.css',
'@mdi/font/css/materialdesignicons.css',
'@/assets/style.less'
],
/*
** Plugins to load before mounting the App
@ -43,7 +40,6 @@ module.exports = {
plugins: [
'@/plugins/i18n.js',
'@/plugins/filters', // text filters, datetime filters, generic transformation helpers etc.
'@/plugins/vuetify', // vuetify
'@/plugins/axios', // axios baseurl configuration
'@/plugins/validators', // inject validators
'@/plugins/api', // api helpers
@ -95,9 +91,33 @@ module.exports = {
}
}
},
buildModules: ['@nuxtjs/vuetify'],
vuetify: {
customVariables: ['~/assets/variables.scss'],
treeShake: true,
theme: {
options: {
customProperties: false,
variations: false,
minifyTheme,
},
dark: true,
themes: {
dark: {
primary: '#FF6E40'
},
light: {
primary: '#FF4500'
}
}
},
defaultAssets: false
},
build: {
corejs: 3,
cache: true,
hardSource: true
hardSource: !isDev,
extractCSS: !isDev,
optimizeCSS: !isDev
},
}

View file

@ -1,12 +1,13 @@
{
"name": "gancio",
"version": "1.2.2",
"version": "1.4.3",
"description": "A shared agenda for local communities",
"author": "lesion",
"scripts": {
"build": "nuxt build --modern",
"start:inspect": "NODE_ENV=production node --inspect node_modules/.bin/nuxt start --modern",
"dev": "nuxt dev",
"test": "cd tests/seeds; jest; cd ../..",
"start": "nuxt start --modern",
"doc": "cd docs && bundle exec jekyll b",
"doc:dev": "cd docs && bundle exec jekyll s --drafts",
@ -26,57 +27,62 @@
"yarn.lock"
],
"dependencies": {
"@mdi/js": "^6.5.95",
"@nuxtjs/auth": "^4.9.1",
"@nuxtjs/axios": "^5.13.5",
"accept-language": "^3.0.18",
"axios": "^0.24.0",
"axios": "^0.26.0",
"bcryptjs": "^2.4.3",
"body-parser": "^1.18.3",
"body-parser": "^1.19.2",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dayjs": "^1.10.7",
"dompurify": "^2.3.3",
"email-templates": "^8.0.8",
"express": "^4.17.1",
"dompurify": "^2.3.6",
"email-templates": "^8.0.9",
"express": "^4.17.3",
"express-oauth-server": "lesion/express-oauth-server#master",
"http-signature": "^1.3.6",
"ical.js": "^1.4.0",
"ical.js": "^1.5.0",
"ics": "^2.35.0",
"jsdom": "^18.1.1",
"jsdom": "^19.0.0",
"jsonwebtoken": "^8.5.1",
"linkify-html": "^3.0.4",
"linkifyjs": "3.0.4",
"linkifyjs": "3.0.5",
"lodash": "^4.17.21",
"mariadb": "^2.5.6",
"microformat-node": "^2.0.1",
"minify-css-string": "^1.0.0",
"mkdirp": "^1.0.4",
"multer": "^1.4.3",
"nuxt-edge": "^2.16.0-27305297.ab1c6cb4",
"pg": "^8.6.0",
"sequelize": "^6.12.0-alpha.1",
"sequelize": "^6.17.0",
"sequelize-slugify": "^1.6.0",
"sharp": "^0.27.2",
"sqlite3": "mapbox/node-sqlite3#918052b",
"tiptap": "^1.32.0",
"tiptap-extensions": "^1.35.0",
"umzug": "^2.3.0",
"v-calendar": "2.3.4",
"v-calendar": "2.4.1",
"vue": "^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",
"vuetify": "npm:@vuetify/nightly@dev",
"winston": "^3.6.0",
"winston-daily-rotate-file": "^4.6.1",
"yargs": "^17.2.0"
},
"devDependencies": {
"@mdi/font": "^6.5.95",
"@nuxtjs/vuetify": "^1.12.3",
"jest": "^27.5.1",
"less": "^4.1.1",
"less-loader": "^7",
"prettier": "^2.3.0",
"pug": "^3.0.2",
"pug-plain-loader": "^1.1.0",
"sass": "^1.43.5",
"sass": "^1.49.4",
"sequelize-cli": "^6.3.0",
"supertest": "^6.2.2",
"webpack": "4",
"webpack-cli": "^4.7.2"
},
@ -87,7 +93,6 @@
"jimp": "0.16.1",
"resize-img": "2.0.0",
"underscore": "1.13.1",
"@nuxtjs/vuetify/**/sass": "1.32.12",
"postcss": "7.0.36",
"glob-parent": "5.1.2",
"chokidar": "3.5.2",

View file

@ -1,7 +1,7 @@
<template lang="pug">
v-container
v-container.container.pa-0.pa-md-3
v-card
v-tabs(v-model='selectedTab')
v-tabs(v-model='selectedTab' show-arrows)
//- SETTINGS
v-tab {{$t('common.settings')}}
@ -49,18 +49,19 @@
</template>
<script>
import { mapState } from 'vuex'
import Users from '../components/admin/Users'
import Events from '../components/admin/Events'
import Places from '../components/admin/Places'
import Settings from '../components/admin/Settings'
import Federation from '../components/admin/Federation'
import Moderation from '../components/admin/Moderation'
import Announcement from '../components/admin/Announcement'
import Theme from '../components/admin/Theme'
export default {
name: 'Admin',
components: { Users, Events, Places, Settings, Federation, Moderation, Announcement, Theme },
components: {
Users: () => import(/* webpackChunkName: "admin" */'../components/admin/Users'),
Events: () => import(/* webpackChunkName: "admin" */'../components/admin/Events'),
Places: () => import(/* webpackChunkName: "admin" */'../components/admin/Places'),
Settings: () => import(/* webpackChunkName: "admin" */'../components/admin/Settings'),
Federation: () => import(/* webpackChunkName: "admin" */'../components/admin/Federation.vue'),
Moderation: () => import(/* webpackChunkName: "admin" */'../components/admin/Moderation.vue'),
Announcement: () => import(/* webpackChunkName: "admin" */'../components/admin/Announcement.vue'),
Theme: () => import(/* webpackChunkName: "admin" */'../components/admin/Theme.vue')
},
middleware: ['auth'],
async asyncData ({ $axios, params, store }) {
try {

View file

@ -1,7 +1,7 @@
<template lang='pug'>
v-container
v-row.mt-5(align='center' justify='center')
v-col(cols='12' md="6" lg="5" xl="4")
v-container.pa-0.pa-md-3
v-row.mt-md-5.ma-0(align='center' justify='center')
v-col.pa-0.pa-md-3(cols='12' md="6" lg="5" xl="4")
v-form(v-model='valid' ref='form' lazy-validation @submit.prevent='submit')
v-card
v-card-title {{$t('common.login')}}
@ -57,7 +57,6 @@ export default {
methods: {
async forgot () {
if (!this.email) {
// this.$root.$message({ message: this.$t('login.insert_email'), color: 'error' })
this.$refs.email.focus()
return
}

View file

@ -1,7 +1,7 @@
<template lang='pug'>
v-container
v-row.mt-5(align='center' justify='center')
v-col(cols='12' md="6" lg="5" xl="4")
v-container.pa-0.pa-md-3
v-row.mt-md-5.ma-0(align='center' justify='center')
v-col.pa-0.pa-md-3(cols='12' md="6" lg="5" xl="4")
v-card
v-card-title {{$t('common.register')}}
@ -28,17 +28,19 @@ v-container
v-btn(@click='register'
:disabled='!valid || loading' :loading='loading'
color='primary') {{$t('common.send')}}
v-icon mdi-chevron-right
v-icon(v-text='mdiChevronRight')
</template>
<script>
import { mapState } from 'vuex'
import get from 'lodash/get'
import { mdiChevronRight } from '@mdi/js'
export default {
name: 'Register',
data () {
return {
mdiChevronRight,
loading: false,
user: {},
valid: true

View file

@ -78,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 => (this.event.id && e.id !== this.event.id) && e.start_datetime >= start && e.start_datetime <= end)
const events = this.events.filter(e => e.start_datetime >= start && e.start_datetime <= end)
return events
},
attributes () {
@ -161,8 +161,10 @@ export default {
this.type = 'normal'
}
this.events = await this.$api.getEvents({
start: dayjs().unix()
start: dayjs().unix(),
show_recurrent: true
})
this.events = this.events.filter(e => e.id !== this.event.id)
},
methods: {
updateRecurrent (value) {
@ -194,7 +196,7 @@ export default {
} else if (what === 'fromHour') {
if (value) {
const [hour, minute] = value.split(':')
const from = dayjs(this.value.from).hour(hour).minute(minute)
const from = dayjs(this.value.from).hour(hour).minute(minute).second(0)
this.$emit('input', { ...this.value, from, fromHour: true })
} else {
this.$emit('input', { ...this.value, fromHour: false })
@ -209,7 +211,7 @@ export default {
if (fromHour > Number(hour) && !this.value.multidate) {
due = due.add(1, 'day')
}
due = due.hour(hour).minute(minute)
due = due.hour(hour).minute(minute).second(0)
this.$emit('input', { ...this.value, due, dueHour: true })
} else {
this.$emit('input', { ...this.value, due: null, dueHour: false })

View file

@ -41,13 +41,14 @@
v-else
:label="$t('common.media')"
:hint="$t('event.media_description')"
prepend-icon="mdi-camera"
:prepend-icon="mdiCamera"
:value='value.image'
@change="selectMedia"
persistent-hint
accept='image/*')
</template>
<script>
import { mdiCamera } from '@mdi/js'
export default {
name: 'MediaInput',
props: {
@ -56,6 +57,7 @@ export default {
},
data () {
return {
mdiCamera,
openMediaDetails: false,
name: this.value.name || '',
focalpoint: this.value.focalpoint || [0, 0],
@ -87,7 +89,7 @@ export default {
},
methods: {
save () {
this.$emit('input', { url: this.value.url, image: this.value.image, name: this.name || this.value.image.name || '', focalpoint: [...this.focalpoint] })
this.$emit('input', { url: this.value.url, image: this.value.image, name: this.name || (this.event.title) || '', focalpoint: [...this.focalpoint] })
this.openMediaDetails = false
},
async remove () {
@ -96,7 +98,7 @@ export default {
this.$emit('remove')
},
selectMedia (v) {
this.$emit('input', { image: v, name: v.name, focalpoint: [0, 0] })
this.$emit('input', { image: v, name: this.event.title, focalpoint: [0, 0] })
},
handleStart (ev) {
ev.preventDefault()

View file

@ -6,23 +6,24 @@
:label="$t('common.where')"
:hint="$t('event.where_description')"
:search-input.sync="placeName"
prepend-icon='mdi-map-marker'
:prepend-icon='mdiMapMarker'
persistent-hint
:value="value.name"
:items="filteredPlaces"
no-filter
item-text='name'
@change='selectPlace')
template(v-slot:item="{ item }")
template(v-slot:item="{ item, attrs, on }")
v-list-item(v-bind='attrs' v-on='on')
v-list-item-content(two-line v-if='item.create')
v-list-item-title <v-icon color='primary'>mdi-plus</v-icon> {{item.name}}
v-list-item-title <v-icon color='primary' v-text='mdiPlus' :aria-label='add'></v-icon> {{item.name}}
v-list-item-content(two-line v-else)
v-list-item-title {{item.name}}
v-list-item-subtitle {{item.address}}
v-list-item-title(v-text='item.name')
v-list-item-subtitle(v-text='item.address')
v-col(cols=12 md=6)
v-text-field(ref='address'
prepend-icon='mdi-map'
:prepend-icon='mdiMap'
:disabled='disableAddress'
:rules="[ v => disableAddress ? true : $validators.required('common.address')(v)]"
:label="$t('common.address')"
@ -32,6 +33,7 @@
</template>
<script>
import { mapState } from 'vuex'
import { mdiMap, mdiMapMarker, mdiPlus } from '@mdi/js'
export default {
name: 'WhereInput',
@ -40,6 +42,7 @@ export default {
},
data () {
return {
mdiMap, mdiMapMarker, mdiPlus,
place: { },
placeName: '',
disableAddress: true

View file

@ -1,11 +1,11 @@
<template lang="pug">
v-container.container.px-0.px-md-3
v-container.container.pa-0.pa-md-3
v-card
v-card-title
h4 {{edit?$t('common.edit_event'):$t('common.add_event')}}
v-spacer
v-btn(link text color='primary' @click='openImportDialog=true')
<v-icon>mdi-file-import</v-icon> {{$t('common.import')}}
<v-icon v-text='mdiFileImport'></v-icon> {{$t('common.import')}}
v-dialog(v-model='openImportDialog' :fullscreen='$vuetify.breakpoint.xsOnly')
ImportDialog(@close='openImportDialog=false' @imported='eventImported')
@ -23,7 +23,7 @@
@change='v => event.title = v'
:value = 'event.title'
:rules="[$validators.required('common.title')]"
prepend-icon='mdi-format-title'
:prepend-icon='mdiFormatTitle'
:label="$t('common.title')"
autofocus
ref='title')
@ -49,11 +49,15 @@
//- tags
v-col(cols=12 md=6)
v-combobox(v-model='event.tags'
prepend-icon="mdi-tag-multiple"
:prepend-icon="mdiTagMultiple"
chips small-chips multiple deletable-chips hide-no-data hide-selected persistent-hint
:delimiters="[',', ' ']"
:delimiters="[',', ';']"
:items="tags.map(t => t.tag)"
:label="$t('common.tags')")
template(v-slot:selection="{ item, on, attrs, selected, parent}")
v-chip(v-bind="attrs" close :close-icon='mdiCloseCircle' @click:close='parent.selectItem(item)'
:input-value="selected" label small) {{item}}
</v-chip>
v-card-actions
v-spacer
@ -64,16 +68,19 @@
<script>
import { mapActions, mapState } from 'vuex'
import dayjs from 'dayjs'
import Editor from '@/components/Editor'
import List from '@/components/List'
import ImportDialog from './ImportDialog'
import DateInput from './DateInput'
import WhereInput from './WhereInput'
import MediaInput from './MediaInput'
import { mdiFileImport, mdiFormatTitle, mdiTagMultiple, mdiCloseCircle } from '@mdi/js'
export default {
name: 'NewEvent',
components: { List, Editor, ImportDialog, MediaInput, WhereInput, DateInput },
components: {
List: () => import(/* webpackChunkName: "add" */'@/components/List'),
Editor: () => import(/* webpackChunkName: "add" */'@/components/Editor'),
ImportDialog: () => import(/* webpackChunkName: "add" */'./ImportDialog.vue'),
MediaInput: () => import(/* webpackChunkName: "add" */'./MediaInput.vue'),
WhereInput: () => import(/* webpackChunkName: "add" */'./WhereInput.vue'),
DateInput: () => import(/* webpackChunkName: "add" */'./DateInput.vue')
},
validate ({ store }) {
return (store.state.auth.loggedIn || store.state.settings.allow_anon_event)
},
@ -114,6 +121,7 @@ export default {
const month = dayjs().month() + 1
const year = dayjs().year()
return {
mdiFileImport, mdiFormatTitle, mdiTagMultiple, mdiCloseCircle,
valid: false,
openImportDialog: false,
event: {

View file

@ -2,7 +2,7 @@
v-container#event.pa-0.pa-sm-2
//- EVENT PAGE
//- gancio supports microformats (http://microformats.org/wiki/h-event)
v-card.h-event
v-card.h-event(itemscope itemtype="https://schema.org/Event")
v-card-actions
//- admin controls
EventAdmin.mb-1(v-if='is_mine' :event='event')
@ -11,32 +11,33 @@ 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' v-if='hasMedia' :src='event | mediaURL')
img.u-featured(v-show='false' v-if='hasMedia' :src='event | mediaURL' itemprop="image")
v-img.main_image.mb-3(
contain
: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')
.p-description.text-body-1.pa-3.rounded(v-if='!hasMedia && event.description' itemprop='description' v-html='event.description')
v-col.col-12.col-lg-4
v-card
v-card(outlined)
v-card-text
v-icon.float-right(v-if='event.parentId' color='success') mdi-repeat
v-icon.float-right(v-if='event.parentId' color='success' v-text='mdiRepeat')
.title.text-h5
b.p-name {{event.title}}
b.p-name(itemprop="name") {{event.title}}
time.dt-start.text-h6(:datetime='event.start_datetime|unixFormat("YYYY-MM-DD HH:mm")')
v-icon mdi-calendar
time.dt-start.text-h6(:datetime='event.start_datetime|unixFormat("YYYY-MM-DD HH:mm")' itemprop="startDate" :content="event.start_datetime|unixFormat('YYYY-MM-DDTHH:mm')")
v-icon(v-text='mdiCalendar')
b.ml-2 {{event|when}}
.d-none.dt-end(itemprop="endDate" :content="event.end_datetime|unixFormat('YYYY-MM-DDTHH:mm')") {{event.end_datetime|unixFormat('YYYY-MM-DD HH:mm')}}
div.text-subtitle-1 {{event.start_datetime|from}}
small(v-if='event.parentId') ({{event|recurrentDetail}})
.text-h6.p-location
v-icon mdi-map-marker
b.vcard.ml-2 {{event.place && event.place.name}}
.text-subtitle-1.adr {{event.place && event.place.address}}
.text-h6.p-location(itemprop="location" itemscope itemtype="https://schema.org/Place")
v-icon(v-text='mdiMapMarker')
b.vcard.ml-2(itemprop="name") {{event.place && event.place.name}}
.text-subtitle-1.adr(itemprop='address') {{event.place && event.place.address}}
//- tags, hashtags
v-card-text(v-if='event.tags.length')
@ -46,21 +47,16 @@ v-container#event.pa-0.pa-sm-2
//- 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' @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} ")
v-btn.ml-2(large icon v-on='on' @click='showEmbed=true' color='primary')
v-icon mdi-code-tags
v-tooltip(bottom) {{$t('common.add_to_calendar')}}
template(v-slot:activator="{on, attrs} ")
v-btn.ml-2(large icon v-on='on' color='primary'
v-btn.ml-2(large icon :title="$t('common.copy_link')" :aria-label="$t('common.copy_link')" color='primary'
@click='clipboard(`${settings.baseurl}/event/${event.slug || event.id}`)')
v-icon(v-text='mdiContentCopy')
v-btn.ml-2(large icon :title="$t('common.embed')" :aria-label="$t('common.embed')" @click='showEmbed=true' color='primary')
v-icon(v-text='mdiCodeTags')
v-btn.ml-2(large icon :title="$t('common.add_to_calendar')" color='primary' :aria-label="$t('common.add_to_calendar')"
:href='`/api/event/${event.slug || event.id}.ics`')
v-icon mdi-calendar-export
v-icon(v-text='mdiCalendarExport')
.p-description.text-body-1.pa-3.rounded(v-if='hasMedia && event.description' v-html='event.description')
.p-description.text-body-1.pa-3.rounded(v-if='hasMedia && event.description' itemprop='description' v-html='event.description')
//- resources from fediverse
#resources.mt-1(v-if='settings.enable_federation')
@ -68,39 +64,41 @@ v-container#event.pa-0.pa-sm-2
//- small.mr-3 🔖 {{event.likes.length}}
//- small {{event.boost.length}}<br/>
v-dialog(v-model='showResources'
fullscreen
destroy-on-close
scrollable
transition='dialog-bottom-transition')
v-dialog(v-model='showResources' max-width="900" width="900" :fullscreen='$vuetify.breakpoint.xsOnly'
destroy-on-close)
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'
v-icon(v-text='mdiClose')
v-carousel.pa-5(:interval='10000'
:next-icon='mdiArrowRight'
:prev-icon='mdiArrowLeft'
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-img(:src='attachment.url' contain max-height='90%')
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.mb-3.resources(v-if='settings.enable_resources' v-for='resource in event.resources'
:key='resource.id' elevation='10' :flat='resource.hidden' outlined)
v-card-title
v-menu(v-if='$auth.user && $auth.user.is_admin' offset-y)
template(v-slot:activator="{ on }")
v-btn.mr-2(v-on='on' color='primary' small icon)
v-icon mdi-dots-vertical
v-icon(v-text='mdiDotsVertical')
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-title <v-icon left v-text='mdiEyeOff'></v-icon> {{$t('admin.hide_resource')}}
v-list-item(v-else @click='hideResource(resource, false)')
v-list-item-title <v-icon left>mdi-eye</v-icon> {{$t('admin.show_resource')}}
v-list-item-title <v-icon left v-text='mdiEye'></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-title <v-icon left v-text='mdiDelete'></v-icon> {{$t('admin.delete_resource')}}
v-list-item(@click='blockUser(resource)')
v-list-item-title <v-icon left>mdi-lock</v-icon> {{$t('admin.block_user')}}
v-list-item-title <v-icon left v-text='mdiLock'></v-icon> {{$t('admin.block_user')}}
v-icon.mr-1(v-show='resource.hidden' v-text='mdiEyeOff')
a(:href='resource.data.url || resource.data.context')
small {{resource.data.published|dateFormat('ddd, D MMMM HH:mm')}}
@ -108,7 +106,8 @@ v-container#event.pa-0.pa-sm-2
v-card-text
div.mt-1(v-html='resource_filter(resource.data.content)')
span(v-for='attachment in resource.data.attachment' :key='attachment.url')
div.d-flex.flex-wrap
span.mr-1(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)'
@ -120,10 +119,10 @@ v-container#event.pa-0.pa-sm-2
.text-center.mt-5.mb-5
v-btn.mr-2(nuxt icon outlined color='primary'
:to='`/event/${event.prev}`' :disabled='!event.prev')
v-icon mdi-arrow-left
v-icon(v-text='mdiArrowLeft')
v-btn(nuxt bottom right outlined icon color='primary'
:to='`/event/${event.next}`' :disabled='!event.next')
v-icon mdi-arrow-right
v-icon(v-text='mdiArrowRight')
v-dialog(v-model='showEmbed' width='700px' :fullscreen='$vuetify.breakpoint.xsOnly')
EmbedEvent(:event='event' @close='showEmbed=false')
@ -131,18 +130,22 @@ v-container#event.pa-0.pa-sm-2
</template>
<script>
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')
import { mdiArrowLeft, mdiArrowRight, mdiDotsVertical, mdiCodeTags, mdiClose,
mdiEye, mdiEyeOff, mdiDelete, mdiRepeat, mdiLock,
mdiCalendarExport, mdiCalendar, mdiContentCopy, mdiMapMarker } from '@mdi/js'
export default {
name: 'Event',
mixins: [clipboard],
components: { EventAdmin, EmbedEvent },
components: {
EventAdmin: () => import(/* webpackChunkName: "event" */'./eventAdmin'),
EmbedEvent: () => import(/* webpackChunkName: "event" */'./embedEvent'),
},
async asyncData ({ $axios, params, error, store }) {
try {
const event = await $axios.$get(`/event/${params.slug}`)
@ -153,6 +156,8 @@ export default {
},
data () {
return {
mdiArrowLeft, mdiArrowRight, mdiDotsVertical, mdiCodeTags, mdiCalendarExport, mdiCalendar,
mdiMapMarker, mdiContentCopy, mdiClose, mdiDelete, mdiEye, mdiEyeOff, mdiRepeat, mdiLock,
currentAttachment: 0,
event: {},
showEmbed: false,

View file

@ -2,10 +2,10 @@
v-card
v-card-title(v-text="$t('common.embed_title')")
v-card-text
v-alert.mb-3.mt-1(type='info' show-icon) {{$t('common.embed_help')}}
v-alert.mb-3.mt-1(type='info' show-icon :icon='mdiInformation') {{$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-icon.ml-1(v-text='mdiContentCopy')
p.mx-auto
.mx-auto
gancio-event(:id='event.id' :baseurl='settings.baseurl')
@ -17,9 +17,13 @@ v-card
<script>
import { mapState } from 'vuex'
import clipboard from '../../assets/clipboard'
import { mdiContentCopy, mdiInformation } from '@mdi/js'
export default {
name: 'EmbedEvent',
data() {
return { mdiContentCopy, mdiInformation }
},
mixins: [clipboard],
props: {
event: { type: Object, default: () => ({}) }

View file

@ -1,22 +1,25 @@
<template lang='pug'>
div
v-btn(text color='primary' v-if='event.is_visible' @click='toggle(false)') {{$t(`common.${event.parentId?'skip':'hide'}`)}}
v-btn(text color='success' v-else @click='toggle(false)') <v-icon color='yellow'>mdi-alert</v-icon> {{$t('common.confirm')}}
v-btn(text color='success' v-else @click='toggle(false)') <v-icon color='yellow' v-text='mdiAlert'></v-icon> {{$t('common.confirm')}}
v-btn(text color='primary' @click='$router.push(`/add/${event.id}`)') {{$t('common.edit')}}
v-btn(text color='primary' v-if='!event.parentId' @click='remove(false)') {{$t('common.remove')}}
template(v-if='event.parentId')
v-divider
span.mr-1 <v-icon>mdi-repeat</v-icon> {{$t('event.edit_recurrent')}}
span.mr-1 <v-icon v-text='mdiRepeat'></v-icon> {{$t('event.edit_recurrent')}}
v-btn(text color='primary' v-if='event.parent.is_visible' @click='toggle(true)') {{$t('common.pause')}}
v-btn(text color='primary' v-else @click='toggle(true)') {{$t('common.start')}}
v-btn(text color='primary' @click='$router.push(`/add/${event.parentId}`)') {{$t('common.edit')}}
v-btn(text color='primary' @click='remove(true)') {{$t('common.remove')}}
</template>
<script>
import { mdiAlert, mdiRepeat } from '@mdi/js'
export default {
name: 'EventAdmin',
data () {
return { mdiAlert, mdiRepeat }
},
props: {
event: {
type: Object,

View file

@ -1,6 +1,6 @@
<template lang="pug">
v-container
v-card(outlined)
v-container.pa-0.pa-md-3
v-card
v-card-title {{$t('common.share')}}
v-card-text
p.text-body-1 {{$t('export.intro')}}
@ -11,7 +11,7 @@
Search(
:filters='filters'
@update='f => filters = f')
v-tabs(v-model='type')
v-tabs(v-model='type' show-arrows)
//- TOFIX
//- v-tab {{$t('common.email')}}
@ -31,7 +31,7 @@
p(v-html='$t(`export.feed_description`)')
v-text-field(v-model='link' readonly)
v-btn(slot='prepend' text color='primary' @click='clipboard(link)') {{$t("common.copy")}}
v-icon.ml-1 mdi-content-copy
v-icon.ml-1(v-text='mdiContentCopy')
v-tab ics/ical
v-tab-item
@ -40,7 +40,7 @@
p(v-html='$t(`export.ical_description`)')
v-text-field(v-model='link')
v-btn(slot='prepend' text color='primary' @click='clipboard(link)') {{$t("common.copy")}}
v-icon.ml-1 mdi-content-copy
v-icon.ml-1(v-text='mdiContentCopy')
v-tab List
v-tab-item
@ -49,19 +49,23 @@
p(v-html='$t(`export.list_description`)')
v-row
v-col.mr-2(:span='11')
v-col.col-12.col-lg-4
v-text-field(v-model='list.title' :label='$t("common.title")')
v-text-field(v-model='list.maxEvents' type='number' min='1' :label='$t("common.max_events")')
v-col.float-right(:span='12')
span {{filters.places.join(',')}}
v-switch(v-model='list.theme' inset true-value='dark' false-value='light' :label="$t('admin.is_dark')")
v-switch(v-model='list.sidebar' inset true-value='true' false-value='false' :label="$t('admin.widget')")
v-col.col-12.col-lg-8
gancio-events(:baseurl='settings.baseurl'
:maxlength='list.maxEvents && Number(list.maxEvents)'
:title='list.title'
:theme='list.theme'
:places='filters.places.join(",")'
:tags='filters.tags.join(",")')
:tags='filters.tags.join(",")'
:show_recurrent='filters.show_recurrent'
:sidebar="list.sidebar")
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-icon.ml-1(v-text='mdiContentCopy')
v-tab(v-if='settings.enable_federation') {{$t('common.fediverse')}}
v-tab-item(v-if='settings.enable_federation')
@ -79,14 +83,17 @@
<script>
import dayjs from 'dayjs'
import { mapState } from 'vuex'
import List from '@/components/List'
import FollowMe from '../components/FollowMe'
import Search from '@/components/Search'
import clipboard from '../assets/clipboard'
import { mdiContentCopy } from '@mdi/js'
export default {
name: 'Exports',
components: { List, FollowMe, Search },
components: {
FollowMe,
Search
},
mixins: [clipboard],
async asyncData ({ $axios, params, store, $api }) {
const events = await $api.getEvents({
@ -95,18 +102,24 @@ export default {
})
return { events }
},
data () {
data ({ $store }) {
return {
mdiContentCopy,
type: 'rss',
notification: { email: '' },
list: { title: 'Gancio', maxEvents: null },
list: {
title: $store.state.settings.title,
maxEvents: null,
theme: $store.state.settings['theme.is_dark'] ? 'dark' : 'light',
sidebar: 'true'
},
filters: { tags: [], places: [], show_recurrent: false },
events: []
}
},
head () {
return {
title: `${this.settings.title} - ${this.$t('common.export')}`
title: `${this.settings.title} - ${this.$t('common.export')}`,
}
},
computed: {
@ -114,7 +127,7 @@ export default {
code () {
const params = [`baseurl="${this.settings.baseurl}"`]
if (this.list.title) {
if (this.list.title && this.list.sidebar === 'true') {
params.push(`title="${this.list.title}"`)
}
@ -127,23 +140,27 @@ export default {
}
if (this.filters.show_recurrent) {
params.push('show_recurrent')
params.push(`show_recurrent="${this.filters.show_recurrent}"`)
}
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`
params.push('sidebar="' + this.list.sidebar + '"')
params.push(`theme="${this.list.theme}"`)
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']
const typeMap = ['rss', 'ics']
const params = []
if (this.filters.tags.length) {
params.push(`tags=${this.filters.tags.join(',')}`)
params.push(`tags=${this.filters.tags.map(encodeURIComponent).join(',')}`)
}
if (this.filters.places.length) {
@ -154,7 +171,7 @@ export default {
params.push('show_recurrent=true')
}
return `${this.settings.baseurl}/feed/${typeMap[this.type]}?${params.join('&')}`
return `${this.settings.baseurl}/feed/${typeMap[this.type]}${params.length ? '?' : ''}${params.join('&')}`
},
showLink () {
return (['rss', 'ics'].includes(this.type))

View file

@ -7,7 +7,7 @@
//- Calendar and search bar
v-row.pt-0.pt-sm-2.pl-0.pl-sm-2
.col-xl-5.col-lg-5.col-md-7.col-sm-12.col-xs-12.pa-4.pa-sm-3
#calh.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(placeholder='Calendar unavailable without js')
@ -15,12 +15,11 @@
.col.pt-0.pt-md-2
Search(:filters='filters' @update='updateFilters')
v-chip(v-if='selectedDay' close @click:close='dayChange({ date: selectedDay})') {{selectedDay}}
v-chip(v-if='selectedDay' close :close-icon='mdiCloseCircle' @click:close='dayChange()') {{selectedDay}}
//- Events
#events.mb-2.mt-1.pl-1.pl-sm-2
//- div.event(v-for='(event, idx) in events' :key='event.id' v-intersect="(entries, observer, isIntersecting) => intersecting[event.id] = isIntersecting")
Event(:event='event' @destroy='destroy' v-for='(event, idx) in visibleEvents' :key='event.id' @tagclick='tagClick' @placeclick='placeClick')
Event(:event='event' @destroy='destroy' v-for='(event, idx) in visibleEvents' :lazy='idx>2' :key='event.id' @tagclick='tagClick' @placeclick='placeClick')
</template>
@ -32,6 +31,7 @@ import Event from '@/components/Event'
import Announcement from '@/components/Announcement'
import Search from '@/components/Search'
import Calendar from '@/components/Calendar'
import { mdiCloseCircle } from '@mdi/js'
export default {
name: 'Index',
@ -47,6 +47,7 @@ export default {
},
data ({ $store }) {
return {
mdiCloseCircle,
first: true,
isCurrentMonth: true,
now: dayjs().unix(),
@ -174,12 +175,7 @@ export default {
this.setFilters(filters)
},
dayChange (day) {
const date = dayjs(day.date).format('YYYY-MM-DD')
if (this.selectedDay === date) {
this.selectedDay = null
return
}
this.selectedDay = date
this.selectedDay = day ? dayjs(day).format('YYYY-MM-DD') : null
}
}
}

View file

@ -2,15 +2,18 @@
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-alert.mb-3.mt-1(outlined type='warning' color='red' show-icon :icon='mdiAlert') {{$t('setup.copy_password_dialog')}}
v-card-actions
v-btn(text @click='next' color='primary' :loading='loading' :disabled='loading') {{$t('setup.start')}}
v-icon mdi-arrow-right
v-icon(v-text='mdiArrowRight')
</template>
<script>
import { mdiArrowRight, mdiAlert } from '@mdi/js'
export default {
data () {
return {
mdiArrowRight, mdiAlert,
loading: false,
user: {
email: 'admin',

View file

@ -6,25 +6,30 @@
v-btn-toggle(text color='primary' v-model='db.dialect')
v-btn(value='sqlite' text) sqlite
v-btn(value='postgres' text) postgres
v-btn(value='mariadb' text) mariadb
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')]")
template(v-if='db.dialect !== "sqlite"')
v-text-field(v-model='db.host' 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
v-icon(v-text='mdiArrowRight')
</template>
<script>
import { mdiArrowRight } from '@mdi/js'
export default {
data () {
return {
mdiArrowRight,
db: {
dialect: 'sqlite',
storage: './gancio.sqlite',
hostname: 'localhost',
host: 'localhost',
database: 'gancio'
},
loading: false

View file

@ -4,14 +4,14 @@
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(v-show='!dbdone' :complete='step > 1' step='1') Database
v-divider(v-show='!dbdone')
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')
v-stepper-content(v-show='!dbdone' step='1')
DbStep(@complete='dbCompleted')
v-stepper-content(step='2')
Settings(setup, @complete='configCompleted')
@ -36,14 +36,16 @@ export default {
title: 'Setup',
},
auth: false,
data () {
asyncData ({ params }) {
return {
dbdone: !!Number(params.db),
config: {
db: {
dialect: ''
}
},
step: 1
step: 1 + Number(params.db)
}
},
methods: {

View file

@ -3,6 +3,8 @@ import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'
import localizedFormat from 'dayjs/plugin/localizedFormat'
import 'dayjs/locale/it'
import 'dayjs/locale/es'
@ -15,6 +17,7 @@ import 'dayjs/locale/fr'
dayjs.extend(relativeTime)
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(localizedFormat)
export default ({ app, store }) => {
// set timezone to instance_timezone!!

View file

@ -4,12 +4,12 @@ import merge from 'lodash/merge'
Vue.use(VueI18n)
export default async ({ app, store, req }) => {
export default async ({ app, store, res }) => {
const messages = {}
if (process.server) {
store.commit('setLocale', req.acceptedLocale)
if (req.user_locale) {
store.commit('setUserLocale', req.user_locale)
store.commit('setLocale', res.locals.acceptedLocale)
if (res.locals.user_locale) {
store.commit('setUserLocale', res.locals.user_locale)
}
}

View file

@ -1,34 +0,0 @@
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'
}
}
}
})
}

12
release.sh Executable file
View file

@ -0,0 +1,12 @@
RELEASE=v$(cat package.json | jq ".version" | sed -e 's/"//g')
echo "Releasing $RELEASE"
yarn build
yarn pack
yarn publish
yarn doc
gpg --detach-sign --local-user 5DAC477D5441B7A15ACBF680BBEB4DD39AC6CCA9 gancio-$RELEASE.tgz
cp gancio-$RELEASE.tgz releases/
mv gancio-$RELEASE.tgz releases/latest.tgz
cp gancio-$RELEASE.tgz.sig releases/
mv gancio-$RELEASE.tgz.sig releases/latest.tgz.sig
rsync -a docs/_site/ gancio.org:/var/www/gancio/

View file

@ -16,24 +16,24 @@ const Auth = {
}
oauth.oauthServer.authenticate()(req, res, () => {
req.user = get(res, 'locals.oauth.token.user', null)
res.locals.user = get(res, 'locals.oauth.token.user', null)
next()
})
},
isAuth (req, res, next) {
if (req.user) {
if (res.locals.user) {
next()
} else {
res.sendStatus(404)
res.sendStatus(403)
}
},
isAdmin (req, res, next) {
if (req.user.is_admin) {
if (res.locals.user && res.locals.user.is_admin) {
next()
} else {
res.status(404)
res.sendStatus(403)
}
},

View file

@ -100,7 +100,7 @@ const eventController = {
async get (req, res) {
const format = req.params.format || 'json'
const is_admin = req.user && req.user.is_admin
const is_admin = res.locals.user && res.locals.user.is_admin
const slug = req.params.event_slug
// retrocompatibility, old events URL does not use slug, use id as fallback
@ -145,21 +145,35 @@ const eventController = {
const next = await Event.findOne({
attributes: ['id', 'slug'],
where: {
id: { [Op.not]: event.id },
is_visible: true,
recurrent: null,
start_datetime: { [Op.gt]: event.start_datetime }
[Op.or]: [
{ start_datetime: { [Op.gt]: event.start_datetime } },
{
start_datetime: event.start_datetime,
id: { [Op.gt]: event.id }
}
]
},
order: [['start_datetime', 'ASC']]
order: [['start_datetime', 'ASC'], ['id', 'ASC']]
})
const prev = await Event.findOne({
attributes: ['id', 'slug'],
where: {
is_visible: true,
id: { [Op.not]: event.id },
recurrent: null,
start_datetime: { [Op.lt]: event.start_datetime }
[Op.or]: [
{ start_datetime: { [Op.lt]: event.start_datetime } },
{
start_datetime: event.start_datetime,
id: { [Op.lt]: event.id }
}
]
},
order: [['start_datetime', 'DESC']]
order: [['start_datetime', 'DESC'], ['id', 'DESC']]
})
// TODO: also check if event is mine
@ -192,7 +206,7 @@ const eventController = {
log.warn(`Trying to confirm a unknown event, id: ${id}`)
return res.sendStatus(404)
}
if (!req.user.is_admin && req.user.id !== event.userId) {
if (!res.locals.user.is_admin && res.locals.user.id !== event.userId) {
log.warn(`Someone unallowed is trying to confirm -> "${event.title} `)
return res.sendStatus(403)
}
@ -218,7 +232,7 @@ const eventController = {
const id = Number(req.params.event_id)
const event = await Event.findByPk(id)
if (!event) { return req.sendStatus(404) }
if (!req.user.is_admin && req.user.id !== event.userId) {
if (!res.locals.user.is_admin && res.locals.user.id !== event.userId) {
return res.sendStatus(403)
}
@ -276,6 +290,13 @@ const eventController = {
res.sendStatus(200)
},
async isAnonEventAllowed (req, res, next) {
if (!res.locals.settings.allow_anon_event) {
return res.sendStatus(403)
}
next()
},
async add (req, res) {
// req.err comes from multer streaming error
if (req.err) {
@ -287,21 +308,23 @@ 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 required_fields = [ 'title', 'place_name', 'start_datetime']
const missing_field = required_fields.find(required_field => !body[required_field])
if (missing_field) {
log.warn(`${missing_field} is required`)
return res.status(400).send(`${missing_field} is required`)
}
const eventDetails = {
title: body.title,
// remove html tags
description: helpers.sanitizeHTML(linkifyHtml(body.description)),
description: helpers.sanitizeHTML(linkifyHtml(body.description || '')),
multidate: body.multidate,
start_datetime: body.start_datetime,
end_datetime: body.end_datetime,
recurrent,
// publish this event only if authenticated
is_visible: !!req.user
is_visible: !!res.locals.user
}
if (req.file || body.image_url) {
@ -344,9 +367,9 @@ const eventController = {
}
// associate user to event and reverse
if (req.user) {
await req.user.addEvent(event)
await event.setUser(req.user)
if (res.locals.user) {
await res.locals.user.addEvent(event)
await event.setUser(res.locals.user)
}
// return created event to the client
@ -368,15 +391,15 @@ const eventController = {
},
async update (req, res) {
if (req.err) {
return res.status(400).json(req.err.toString())
if (res.locals.err) {
return res.status(400).json(res.locals.err.toString())
}
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) {
if (!res.locals.user.is_admin && event.userId !== res.locals.user.id) {
return res.sendStatus(403)
}
@ -456,7 +479,7 @@ const eventController = {
async remove (req, res) {
const event = await Event.findByPk(req.params.id)
// check if event is mine (or user is admin)
if (event && (req.user.is_admin || req.user.id === event.userId)) {
if (event && (res.locals.user.is_admin || res.locals.user.id === event.userId)) {
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)
@ -504,13 +527,19 @@ const eventController = {
where.start_datetime = { [Op.lte]: end }
}
if (places) {
where.placeId = places.split(',')
if (tags && places) {
where[Op.or] = {
placeId: places ? places.split(',') : [],
'$tags.tag$': tags.split(',')
}
}
let where_tags = {}
if (tags) {
where_tags = { where: { [Op.or]: { tag: tags.split(',') } } }
where['$tags.tag$'] = tags.split(',')
}
if (places) {
where.placeId = places.split(',')
}
const events = await Event.findAll({
@ -526,7 +555,6 @@ const eventController = {
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'] }
@ -548,13 +576,15 @@ const eventController = {
* Select events based on params
*/
async select (req, res) {
const settings = res.locals.settings
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)
const show_recurrent = settings.allow_recurrent_event &&
typeof req.query.show_recurrent !== 'undefined' ? req.query.show_recurrent === 'true' : settings.recurrent_event_visible
res.json(await eventController._select({
start, end, places, tags, show_recurrent, max
@ -563,9 +593,8 @@ const eventController = {
/**
* Ensure we have the next instance of a recurrent event
* TODO: create a future instance if the next one is skipped
*/
_createRecurrentOccurrence (e) {
async _createRecurrentOccurrence (e, startAt) {
log.debug(`Create recurrent event [${e.id}] ${e.title}"`)
const event = {
parentId: e.id,
@ -579,23 +608,20 @@ const eventController = {
const recurrent = e.recurrent
const start_date = dayjs.unix(e.start_datetime)
const now = dayjs()
let cursor = start_date > now ? start_date : now
let cursor = start_date > startAt ? start_date : startAt
startAt = cursor
const duration = dayjs.unix(e.end_datetime).diff(start_date, 's')
const frequency = recurrent.frequency
const type = recurrent.type
log.info(`NOW IS ${cursor} while event is at ${start_date} (freq: ${frequency})`)
cursor = cursor.hour(start_date.hour()).minute(start_date.minute()).second(0)
log.info(`set cursor to correct date and hour => ${cursor}`)
if (!frequency) { return }
// each week or 2
if (frequency[1] === 'w') {
cursor = cursor.day(start_date.day())
if (cursor.isBefore(dayjs())) {
if (cursor.isBefore(startAt)) {
cursor = cursor.add(7, 'day')
}
if (frequency[0] === '2') {
@ -605,24 +631,24 @@ const eventController = {
if (type === 'ordinal') {
cursor = cursor.date(start_date.date())
if (cursor.isBefore(dayjs())) {
if (cursor.isBefore(startAt)) {
cursor = cursor.add(1, 'month')
}
} else { // weekday
// get weekday
log.info(type)
// get recurrent freq details
cursor = helpers.getWeekdayN(cursor, type, start_date.day())
if (cursor.isBefore(dayjs())) {
if (cursor.isBefore(startAt)) {
cursor = cursor.add(4, 'week')
cursor = helpers.getWeekdayN(cursor, type, start_date.day())
}
}
}
log.info(cursor)
log.debug(cursor)
event.start_datetime = cursor.unix()
event.end_datetime = event.start_datetime + duration
Event.create(event)
const newEvent = await Event.create(event)
return newEvent.addTags(e.tags)
},
/**
@ -632,13 +658,19 @@ const eventController = {
// select recurrent events and its childs
const events = await Event.findAll({
where: { is_visible: true, recurrent: { [Op.ne]: null } },
include: [{ model: Event, as: 'child', required: false, where: { start_datetime: { [Op.gte]: start_datetime } } }],
order: ['start_datetime']
include: [{ model: Tag, required: false },
{ model: Event, as: 'child', required: false, where: { start_datetime: { [Op.gte]: start_datetime } }}],
order: [['child', 'start_datetime', 'DESC']]
})
// create a new occurrence for each recurring events but the one's that has an already visible occurrence coming
const creations = events.map(e => {
if (e.child.length) {
if (e.child.find(c => c.is_visible)) return
return eventController._createRecurrentOccurrence(e, dayjs.unix(e.child[0].start_datetime+1))
}
return eventController._createRecurrentOccurrence(e, dayjs())
})
// filter events that as no instance in future yet
const creations = events
.filter(e => e.child.length === 0)
.map(eventController._createRecurrentOccurrence)
return Promise.all(creations)
}

View file

@ -2,7 +2,7 @@ const Event = require('../models/event')
const Place = require('../models/place')
const Tag = require('../models/tag')
const { Op } = require('sequelize')
const { Op, literal } = require('sequelize')
const moment = require('dayjs')
const ics = require('ics')
@ -16,10 +16,17 @@ const exportController = {
const where = {}
const yesterday = moment().subtract('1', 'day').unix()
let where_tags = {}
if (tags && places) {
where[Op.or] = {
placeId: places ? places.split(',') : [],
'$tags.tag$': tags.split(',')
}
}
if (tags) {
where_tags = { where: { tag: tags.split(',') } }
where['$tags.tag$'] = tags.split(',')
}
if (places) {
@ -39,7 +46,15 @@ const exportController = {
start_datetime: { [Op.gte]: yesterday },
...where
},
include: [{ model: Tag, required: false, ...where_tags }, { model: Place, attributes: ['name', 'id', 'address'] }]
include: [
{
model: Tag,
order: [literal('(SELECT COUNT("tagTag") FROM event_tags WHERE tagTag = tag) DESC')],
attributes: ['tag'],
required: !!tags,
through: { attributes: [] }
},
{ model: Place, attributes: ['name', 'id', 'address'] }]
})
switch (type) {
@ -54,8 +69,9 @@ const exportController = {
},
feed (req, res, events) {
const settings = res.locals.settings
res.type('application/rss+xml; charset=UTF-8')
res.render('feed/rss.pug', { events, settings: req.settings, moment })
res.render('feed/rss.pug', { events, settings, moment })
},
/**
@ -64,6 +80,7 @@ const exportController = {
* @param {*} alarms https://github.com/adamgibbons/ics#attributes (alarms)
*/
ics (req, res, events, alarms = []) {
const settings = res.locals.settings
const eventsMap = events.map(e => {
const tmpStart = moment.unix(e.start_datetime)
const tmpEnd = moment.unix(e.end_datetime)
@ -74,10 +91,10 @@ const exportController = {
// startOutputType: 'utc',
end,
// endOutputType: 'utc',
title: `[${req.settings.title}] ${e.title}`,
title: `[${settings.title}] ${e.title}`,
description: e.description,
location: `${e.place.name} - ${e.place.address}`,
url: `${req.settings.baseurl}/event/${e.slug || e.id}`,
url: `${settings.baseurl}/event/${e.slug || e.id}`,
alarms
}
})

View file

@ -63,7 +63,7 @@ const oauthController = {
async getClients (req, res) {
const tokens = await OAuthToken.findAll({
include: [{ model: User, where: { id: req.user.id } }, { model: OAuthClient, as: 'client' }],
include: [{ model: User, where: { id: res.locals.user.id } }, { model: OAuthClient, as: 'client' }],
raw: true,
nest: true
})

View file

@ -35,9 +35,11 @@ const resourceController = {
},
event: {
id: r.event.id,
title: r.event.title
title: r.event.title,
slug: r.event.slug
},
ap_user: {
url: get(r, 'ap_user.object.url', ''),
ap_id: get(r, 'ap_user.ap_id', ''),
preferredUsername: get(r, 'ap_user.object.preferredUsername', '')
}

View file

@ -9,6 +9,7 @@ const pkg = require('../../../package.json')
const generateKeyPair = promisify(crypto.generateKeyPair)
const log = require('../../log')
const locales = require('../../../locales/index')
const escape = require('lodash/escape')
let defaultHostname
@ -54,7 +55,7 @@ const settingsController = {
secretSettings: {},
async load () {
if (config.firstrun) {
if (config.status !== 'READY') {
settingsController.settings = defaultSettings
return
}
@ -109,7 +110,7 @@ const settingsController = {
// load custom plugins
const plugins_path = path.resolve(process.env.cwd || '', 'plugins')
if (fs.existsSync(plugins_path)) {
if (process.env.NODE_ENV === 'production' && fs.existsSync(plugins_path)) {
const notifier = require('../../notifier')
const pluginsFile = fs.readdirSync(plugins_path).filter(e => path.extname(e).toLowerCase() === '.js')
pluginsFile.forEach( pluginFile => {
@ -162,11 +163,12 @@ const settingsController = {
await settingsController.set('smtp', smtp.smtp)
const mail = require('../mail')
try {
await mail._send(settingsController.settings.admin_email, 'test', null, 'en')
await mail._send(settingsController.settings.admin_email, 'test')
return res.sendStatus(200)
} catch (e) {
console.error(e)
return res.status(400).send(String(e))
return res.status(400).send(escape(String(e)))
}
},
@ -190,11 +192,6 @@ const settingsController = {
settingsController.set('logo', baseImgPath)
res.sendStatus(200)
})
},
getAllRequest (req, res) {
// get public settings and public configuration
res.json({ ...settingsController.settings, version: pkg.version })
}
}

View file

@ -5,21 +5,22 @@ const db = require('../models/index.js')
const config = require('../../config')
const settingsController = require('./settings')
const path = require('path')
const escape = require('lodash/escape')
const setupController = {
async setupDb (req, res, next) {
log.debug('[SETUP] Check db')
const dbConf = req.body.db
async _setupDb (dbConf) {
if (!dbConf) {
return res.sendStatus(400)
throw Error('Empty DB configuration')
}
if (dbConf.storage) {
if (dbConf.dialect === 'sqlite' && dbConf.storage) {
dbConf.storage = path.resolve(process.env.cwd || '', dbConf.storage)
} else {
dbConf.storage = ''
}
try {
// try to connect
dbConf.logging = false
await db.connect(dbConf)
@ -28,29 +29,37 @@ const setupController = {
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.')
throw Error(' ⚠ Non empty db! Please move your current db elsewhere than retry.')
}
await db.runMigrations()
config.db = dbConf
config.firstrun = false
config.status = 'DBCONF'
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)
},
async setupDb (req, res) {
log.debug('[SETUP] Check db')
const dbConf = req.body.db
try {
await setupController._setupDb(dbConf)
} catch (e) {
return res.status(400).send(String(e))
}
return res.sendStatus(200)
},
async restart (req, res) {
try {
config.baseurl = req.protocol + '://' + req.headers.host
config.hostname = new URL.URL(config.baseurl).hostname
// write configuration
config.write()
@ -72,12 +81,13 @@ const setupController = {
log.info('Restart needed')
res.end()
// exit process so pm2 || docker could restart me || service
process.kill(process.pid)
setTimeout(() => process.kill(process.pid), 1000)
} catch (e) {
log.error(String(e))
return res.status(400).send(String(e))
return res.status(400).send(escape(String(e)))
}
}

View file

@ -15,7 +15,7 @@ const userController = {
if (!user) { return res.sendStatus(200) }
user.recover_code = crypto.randomBytes(16).toString('hex')
mail.send(user.email, 'recover', { user, config }, req.settings.locale)
mail.send(user.email, 'recover', { user, config }, res.locals.locale)
await user.save()
res.sendStatus(200)
@ -44,13 +44,13 @@ const userController = {
},
async current (req, res) {
if (!req.user) { return res.status(400).send('Not logged') }
const user = await User.scope('withoutPassword').findByPk(req.user.id)
if (!res.locals.user) { return res.status(400).send('Not logged') }
const user = await User.scope('withoutPassword').findByPk(res.locals.user.id)
res.json(user)
},
async getAll (req, res) {
const users = await User.scope(req.user.is_admin ? 'withRecover' : 'withoutPassword').findAll({
const users = await User.scope(res.locals.user.is_admin ? 'withRecover' : 'withoutPassword').findAll({
order: [['is_admin', 'DESC'], ['createdAt', 'DESC']]
})
res.json(users)
@ -62,14 +62,14 @@ const userController = {
if (!user) { return res.status(404).json({ success: false, message: 'User not found!' }) }
if (req.body.id !== req.user.id && !req.user.is_admin) {
if (req.body.id !== res.locals.user.id && !res.locals.user.is_admin) {
return res.status(400).json({ succes: false, message: 'Not allowed' })
}
if (!req.body.password) { delete req.body.password }
if (!user.is_active && req.body.is_active && user.recover_code) {
mail.send(user.email, 'confirm', { user, config }, req.settings.locale)
mail.send(user.email, 'confirm', { user, config }, res.locals.settings.locale)
}
await user.update(req.body)
@ -99,7 +99,7 @@ const userController = {
log.info('Register user ', req.body.email)
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(user.email, 'register', { user, config }, res.locals.locale)
mail.send(settingsController.settings.admin_email, 'admin_register', { user, config })
res.sendStatus(200)
} catch (e) {
@ -113,7 +113,7 @@ const userController = {
req.body.is_active = true
req.body.recover_code = crypto.randomBytes(16).toString('hex')
const user = await User.scope('withRecover').create(req.body)
mail.send(user.email, 'user_confirm', { user, config }, req.settings.locale)
mail.send(user.email, 'user_confirm', { user, config }, res.locals.locale)
res.json(user)
} catch (e) {
log.error('User creation error:', e)

View file

@ -10,7 +10,7 @@ api.use(express.urlencoded({ extended: false }))
api.use(express.json())
if (config.firstrun) {
if (config.status !== 'READY') {
const setupController = require('./controller/setup')
const settingsController = require('./controller/settings')
@ -56,7 +56,7 @@ if (config.firstrun) {
```
*/
api.get('/ping', (req, res) => res.sendStatus(200))
api.get('/user', isAuth, (req, res) => res.json(req.user))
api.get('/user', isAuth, (req, res) => res.json(res.locals.user))
api.post('/user/recover', userController.forgotPassword)
@ -80,10 +80,28 @@ if (config.firstrun) {
// update a place (modify address..)
api.put('/place', isAdmin, eventController.updatePlace)
/**
* Get events
* @category Event
* @name /api/events
* @type GET
* @param {integer} [start] - start timestamp (default: now)
* @param {integer} [end] - end timestamp (optional)
* @param {array} [tags] - List of tags
* @param {array} [places] - List of places
* @param {integer} [max] - Max events
* @param {boolean} [show_recurrent] - Show also recurrent events (default: as choosen in admin settings)
* @example ***Example***
* [https://demo.gancio.org/api/events](https://demo.gancio.org/api/events)
* [usage example](https://framagit.org/les/gancio/-/blob/master/webcomponents/src/GancioEvents.svelte#L18-42)
*/
api.get('/events', cors, eventController.select)
/**
* Add a new event
* @category Event
* @name /event
* @name /api/event
* @type POST
* @info `Content-Type` has to be `multipart/form-data` to support image upload
* @param {string} title - event's title
@ -95,13 +113,12 @@ if (config.firstrun) {
* @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.post('/event', eventController.isAnonEventAllowed, upload.single('image'), eventController.add)
api.put('/event', isAuth, upload.single('image'), eventController.update)
api.get('/event/import', isAuth, helpers.importURL)
@ -119,7 +136,6 @@ if (config.firstrun) {
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)
@ -134,9 +150,6 @@ if (config.firstrun) {
// 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)

View file

@ -7,7 +7,8 @@ const { Task, TaskManager } = require('../taskManager')
const locales = require('../../locales')
const mail = {
send (addresses, template, locals, locale = settingsController.settings.instance_locale) {
send (addresses, template, locals, locale) {
locale = locale || settingsController.settings.instance_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
@ -21,7 +22,8 @@ const mail = {
TaskManager.add(task)
},
_send (addresses, template, locals, locale = settingsController.settings.instance_locale) {
_send (addresses, template, locals, locale) {
locale = locale || settingsController.settings.instance_locale
const settings = settingsController.settings
log.info(`Send ${template} email to ${addresses} with locale ${locale}`)
const email = new Email({

View file

@ -3,6 +3,7 @@ const Umzug = require('umzug')
const path = require('path')
const config = require('../../config')
const log = require('../../log')
const settingsController = require('../controller/settings')
const db = {
sequelize: null,
@ -13,6 +14,7 @@ const db = {
},
connect (dbConf = config.db) {
log.debug(`Connecting to DB: ${JSON.stringify(dbConf)}`)
dbConf.dialectOptions = { autoJsonMap: false }
db.sequelize = new Sequelize(dbConf)
return db.sequelize.authenticate()
},
@ -21,7 +23,7 @@ const db = {
return !(users && users.length)
},
async runMigrations () {
const logging = config.firstrun ? false : log.debug.bind(log)
const logging = config.status !== 'READY' ? false : log.debug.bind(log)
const umzug = new Umzug({
storage: 'sequelize',
storageOptions: { sequelize: db.sequelize },
@ -40,11 +42,12 @@ const db = {
return await umzug.up()
},
async initialize () {
if (!config.firstrun) {
if (config.status === 'READY') {
try {
await db.connect()
log.debug('Running migrations')
return db.runMigrations()
await db.runMigrations()
return settingsController.load()
} catch (e) {
log.warn(` ⚠️ Cannot connect to db, check your configuration => ${e}`)
process.exit(1)

View file

@ -12,11 +12,11 @@ const oauthServer = new OAuthServer({
debug: true,
requireClientAuthentication: { password: false },
authenticateHandler: {
handle (req) {
if (!req.user) {
handle (req, res) {
if (!res.locals.user) {
throw new Error('Not authenticated!')
}
return req.user
return res.locals.user
}
}
})
@ -34,7 +34,7 @@ oauth.use((req, res) => res.sendStatus(404))
oauth.use((err, req, res, next) => {
const error_msg = err.toString()
log.error('[OAUTH USE] ' + error_msg)
log.warn('[OAUTH USE] ' + error_msg)
res.status(500).send(error_msg)
})

View file

@ -19,7 +19,7 @@ const DiskStorage = {
const thumbStream = fs.createWriteStream(thumbPath)
const resizer = sharp().resize(1200).jpeg({ quality: 98 })
const thumbnailer = sharp().resize(400).jpeg({ quality: 90 })
const thumbnailer = sharp().resize(500).jpeg({ quality: 98 })
let onError = false
const err = e => {
if (onError) {

View file

@ -1,6 +1,7 @@
#!/usr/bin/env node
const pkg = require('../package.json')
const path = require('path')
const accountsCLI = require('./cli/accounts')
process.env.cwd = process.env.GANCIO_DATA || path.resolve('./')
@ -28,6 +29,7 @@ require('yargs')
process.env.config_path = absolute_config_path
return absolute_config_path
})
.command(['accounts'], 'Manage accounts', accountsCLI)
.command(['start', 'run', '$0'], 'Start gancio', {}, start)
.help('h')
.alias('h', 'help')

54
server/cli/accounts.js Normal file
View file

@ -0,0 +1,54 @@
function _initializeDB () {
const config = require('../config')
config.load()
config.log_level = 'error'
const db = require('../api/models/index')
return db.initialize()
}
async function modify (args) {
await _initializeDB()
const helpers = require('../helpers')
const User = require('../api/models/user')
const user = await User.findOne({ where: { email: args.account } })
console.log()
if (!user) {
console.error(`User ${args.account} not found`)
return
}
if (args['reset-password']) {
const password = helpers.randomString()
user.password = password
await user.save()
console.log(`New password for user ${user.email} is '${password}'`)
}
}
async function add (args) {
}
async function list () {
await _initializeDB()
const User = require('../api/models/user')
const users = await User.findAll()
console.log()
users.forEach(u => console.log(`${u.id}\tadmin: ${u.is_admin}\tenabled: ${u.is_active}\temail: ${u.email} - ${u.password}`))
console.log()
}
const accountsCLI = yargs => {
return yargs
.command('list', 'List all accounts', list)
.command('modify', 'Modify', {
account: {
describe: 'Account to modify'
},
'reset-password': {
describe: 'Resets the password of the given accoun '
}
}, modify)
.command('add', 'Add an account', {}, add)
}
module.exports = accountsCLI

View file

@ -3,7 +3,7 @@ const path = require('path')
const URL = require('url')
let config = {
firstrun: true,
status: 'SETUP',
baseurl: '',
hostname: '',
server: {
@ -15,7 +15,7 @@ let config = {
db: {},
upload_path: path.resolve(process.env.cwd || '', 'uploads'),
write (config_path= process.env.config_path || './config.json') {
delete config.firstrun
delete config.status
return fs.writeFileSync(config_path, JSON.stringify(config, null, 2))
},
@ -26,17 +26,15 @@ let config = {
if (fs.existsSync(config_path)) {
const configContent = fs.readFileSync(config_path)
config = Object.assign(config, JSON.parse(configContent))
config.firstrun = false
config.status = 'READY'
if (!config.hostname) {
config.hostname = new URL.URL(config.baseurl).hostname
}
} else {
config.firstrun = true
config.status = 'SETUP'
console.info('> Configuration file does not exists, running setup..')
}
}
}
config.load()
module.exports = config

View file

@ -2,15 +2,17 @@ const config = require('../config')
const Helpers = require('./helpers')
const crypto = require('crypto')
const log = require('../log')
const settingsController = require('../api/controller/settings')
module.exports = {
// follow request from fediverse
async follow (req, res) {
const body = req.body
const settings = res.locals.settings
if (typeof body.object !== 'string') { return }
const username = body.object.replace(`${config.baseurl}/federation/u/`, '')
if (username !== req.settings.instance_name) {
log.warn(`Following the wrong user: ${username} instead of ${req.settings.instance_name} (could be a wrong config.baseurl)`)
if (username !== settings.instance_name) {
log.warn(`Following the wrong user: ${username} instead of ${settings.instance_name} (could be a wrong config.baseurl)`)
return res.status(404).send('User not found')
}
@ -18,7 +20,7 @@ module.exports = {
// if (!user.followers.includes(body.actor)) {
// await user.addFollowers([req.fedi_user.id])
// await user.update({ followers: [...user.followers, body.actor] })
await req.fedi_user.update({ follower: true })
await res.locals.fedi_user.update({ follower: true })
log.info(`Followed by ${body.actor}`)
const guid = crypto.randomBytes(16).toString('hex')
const message = {
@ -28,24 +30,25 @@ module.exports = {
actor: `${config.baseurl}/federation/u/${username}`,
object: body
}
Helpers.signAndSend(JSON.stringify(message), req.fedi_user.object.inbox)
Helpers.signAndSend(JSON.stringify(message), res.locals.fedi_user.object.inbox)
res.sendStatus(200)
},
// unfollow request from fediverse
async unfollow (req, res) {
const settings = res.locals.settings
const body = req.body
const username = body.object.object.replace(`${config.baseurl}/federation/u/`, '')
if (username !== req.settings.instance_name) {
log.warn(`Unfollowing wrong user: ${username} instead of ${req.settings.instance_name}`)
if (username !== settings.instance_name) {
log.warn(`Unfollowing wrong user: ${username} instead of ${settings.instance_name}`)
return res.status(404).send('User not found')
}
if (body.actor !== body.object.actor || body.actor !== req.fedi_user.ap_id) {
if (body.actor !== body.object.actor || body.actor !== res.locals.fedi_user.ap_id) {
log.info('Unfollow an user created by a different actor !?!?')
return res.status(400).send('Bad things')
}
await req.fedi_user.update({ follower: false })
await res.locals.fedi_user.update({ follower: false })
log.info(`Unfollowed by ${body.actor}`)
res.sendStatus(200)
}

View file

@ -192,7 +192,7 @@ const Helpers = {
return res.status(401).send('User blocked')
}
req.fedi_user = user
res.locals.fedi_user = user
// TODO: check Digest // cannot do this with json bodyparser
// const digest = crypto.createHash('sha256')

View file

@ -6,6 +6,7 @@ const Event = require('../api/models/event')
const User = require('../api/models/user')
const Tag = require('../api/models/tag')
const Place = require('../api/models/place')
const settingsController = require('../api/controller/settings')
const Helpers = require('./helpers')
const Inbox = require('./inbox')
@ -20,7 +21,6 @@ router.use(cors())
// is federation enabled? middleware
router.use((req, res, next) => {
const settingsController = require('../api/controller/settings')
if (settingsController.settings.enable_federation) { return next() }
log.debug('Federation disabled!')
return res.status(401).send('Federation disabled')
@ -36,7 +36,7 @@ router.get('/m/:event_id', async (req, res) => {
const event = await Event.findByPk(req.params.event_id, { include: [User, Tag, Place] })
if (!event) { return res.status(404).send('Not found') }
return res.json(event.toAP(settingsController.settings.instance_name, req.settings.instance_locale))
return res.json(event.toAP(settingsController.settings.instance_name, settingsController.settings.instance_locale))
})
// get any message coming from federation

Some files were not shown because too many files have changed in this diff Show more