Merge branch 'master' into gh

This commit is contained in:
lesion
2022-06-06 16:57:05 +02:00
110 changed files with 5909 additions and 2199 deletions

2
.gitignore vendored
View File

@@ -1,6 +1,8 @@
# Created by .ignore support plugin (hsz.mobi) # Created by .ignore support plugin (hsz.mobi)
### Gancio dev configuration ### Gancio dev configuration
tests/seeds/testdb.sqlite
preso.md
gancio.sqlite gancio.sqlite
db.sqlite db.sqlite
releases releases

View File

@@ -1,7 +1,37 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
### UNRELEASED ### 1.5.0 - UNRELEASED
- add CLI support to manage accounts (list / modify / add accounts) - new Tag page!
- new Place page!
- new search flow
- new meta-tag-place / group / cohort page!
- allow footer links reordering
- new Docker image
- add GANCIO_DB_PORT environment
- merge old duplicated tags, trim
- add dynamic sitemap.xml !
- calendar attributes refactoring (a dot each day, colors represents n. events)
- fix event mime type response
### 1.4.4 - 10 may '22
- better img rendering, make it easier to download flyer #153
- avoid place and tags duplication (remove white space, match case insensitive)
- show date and place to unconfirmed events
- add warning when visiting from different hostname or protocol #149
- add tags and fix html description in ics export
- add git dependency in Dockerfile #148
- add external_style param to gancio-events webcomponent
- add GANCIO_HOST and GANCIO_PORT environment vars
- fix place and address when importing from url #147
- fix user account removal
- fix timezone issue #151
- fix scrolling behavior
- fix adding event on disabled anon posting
- fix plain description meta
- fix recurrent events always shown #150
- remove `less` and `less-loader` dependency
### 1.4.3 - 10 mar '22 ### 1.4.3 - 10 mar '22
- fix [#140](https://framagit.org/les/gancio/-/issues/140) - Invalid date - fix [#140](https://framagit.org/les/gancio/-/issues/140) - Invalid date
- fix [#141](https://framagit.org/les/gancio/-/issues/141) - Cannot change logo - fix [#141](https://framagit.org/les/gancio/-/issues/141) - Cannot change logo

View File

@@ -0,0 +1,6 @@
/**
* https://nuxtjs.org/docs/configuration-glossary/configuration-router/#scrollbehavior
*/
export default function (to, _from, savedPosition) {
return { x: 0, y: 0 }
}

1262
assets/gancio-events.es.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,56 +1,39 @@
import take from 'lodash/take'
import get from 'lodash/get'
import dayjs from 'dayjs' import dayjs from 'dayjs'
export function attributesFromEvents (_events, _tags) { export function attributesFromEvents (_events) {
const colors = ['blue', 'orange', 'yellow', 'teal', 'indigo', 'green', 'red', 'purple', 'pink', 'gray']
const tags = take(_tags, 10).map(t => t.tag) // const colors = ['teal', 'green', 'yellow', 'teal', 'indigo', 'green', 'red', 'purple', 'pink', 'gray']
// merge events with same date
let attributes = [] let attributes = []
attributes.push({ key: 'today', dates: new Date(), bar: { color: 'green', fillMode: 'outline' } })
const now = dayjs().unix() const now = dayjs().unix()
for(let e of _events) {
const key = dayjs.unix(e.start_datetime).format('YYYYMMDD')
const c = e.start_datetime < now ? 'vc-past' : ''
function getColor (event, where) { const i = attributes.find(a => a.day === key)
const color = { class: 'vc-rounded-full', color: 'blue', fillMode: where === 'base' ? 'light' : 'solid' } if (!i) {
const tag = get(event, 'tags[0]') attributes.push({ day: key, key: e.id, n: 1, dates: new Date(e.start_datetime * 1000),
if (event.start_datetime < now) { dot: { color: 'teal', class: c } })
if (event.multidate) { continue
color.fillMode = where === 'base' ? 'light' : 'outline'
if (where === 'base') {
color.class += ' vc-past'
} }
i.n++
if (i.n >= 20 ) {
i.dot = { color: 'purple', class: c }
} else if ( i.n >= 10 ) {
i.dot = { color: 'red', class: c}
} else if ( i.n >= 5 ) {
i.dot = { color: 'orange', class: c}
} else if ( i.n >= 3 ) {
i.dot = { color: 'yellow', class: c}
} else { } else {
color.class += ' vc-past' i.dot = { color: 'teal', class: c }
}
}
if (!tag) { return color }
const idx = tags.indexOf(tag)
if (idx < 0) { return color }
color.color = colors[idx]
// if (event.start_datetime < now) { color.class += ' vc-past' }
return color
} }
attributes = attributes.concat(_events
.filter(e => !e.multidate)
.map(e => {
return {
key: e.id,
dot: getColor(e),
dates: new Date(e.start_datetime * 1000)
} }
}))
attributes = attributes.concat(_events // add a bar to highlight today
.filter(e => e.multidate) attributes.push({ key: 'today', dates: new Date(), highlight: { color: 'green', fillMode: 'outline' } })
.map(e => ({
key: e.id,
highlight: {
start: getColor(e),
base: getColor(e, 'base'),
end: getColor(e)
},
dates: { start: new Date(e.start_datetime * 1000), end: new Date(e.end_datetime * 1000) }
})))
return attributes return attributes
} }

View File

@@ -31,7 +31,8 @@ li {
} }
#calh { #calh {
height: 292px; /* this is to avoid content shift layout as v-calendar does not support SSR */
height: 268px;
} }
.container { .container {
@@ -55,7 +56,6 @@ li {
scrollbar-color: #FF4511 #111; scrollbar-color: #FF4511 #111;
} }
// EVENT
.event { .event {
display: flex; display: flex;
position: relative; position: relative;
@@ -67,8 +67,9 @@ li {
margin-right: .4em; margin-right: .4em;
transition: all .5s; transition: all .5s;
overflow: hidden; overflow: hidden;
}
.title { .event .title {
display: -webkit-box; display: -webkit-box;
overflow: hidden; overflow: hidden;
margin: 0.5rem 1rem 0.5rem 1rem; margin: 0.5rem 1rem 0.5rem 1rem;
@@ -77,33 +78,22 @@ li {
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
font-size: 1.1em !important; font-size: 1.1em !important;
line-height: 1.2em !important; line-height: 1.2em !important;
text-decoration: none;
} }
.body { .event .body {
flex: 1 1 auto; flex: 1 1 auto;
} }
.img { .event .place span {
width: 100%;
max-height: 250px;
min-height: 160px;
object-fit: cover;
object-position: top;
aspect-ratio: 1.7778;
}
.place {
max-width: 100%;
span {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
}
a { .event a {
text-decoration: none; text-decoration: none;
} }
}
.vc-past { .vc-past {
opacity: 0.4; opacity: 0.4;

View File

@@ -1,8 +0,0 @@
// 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,7 +1,7 @@
<template lang="pug"> <template>
nuxt-link(:to='`/announcement/${announcement.id}`') <nuxt-link :to='`/announcement/${announcement.id}`'>
v-alert.mb-1(border='left' type='info' color="primary" :icon='mdiInformation') {{announcement.title}} <v-alert class='mb-1' outlined type='info' color="primary" :icon='mdiInformation'>{{announcement.title}}</v-alert>
</nuxt-link>
</template> </template>
<script> <script>
import { mdiInformation } from '@mdi/js' import { mdiInformation } from '@mdi/js'

View File

@@ -16,7 +16,7 @@
</template> </template>
<script> <script>
import { mapState, mapActions } from 'vuex' import { mapState } from 'vuex'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { attributesFromEvents } from '../assets/helper' import { attributesFromEvents } from '../assets/helper'
@@ -26,25 +26,22 @@ export default {
events: { type: Array, default: () => [] } events: { type: Array, default: () => [] }
}, },
data () { data () {
const month = dayjs().month() + 1 const month = dayjs.tz().month() + 1
const year = dayjs().year() const year = dayjs.tz().year()
return { return {
selectedDate: null, selectedDate: null,
page: { month, year } page: { month, year }
} }
}, },
computed: { computed: {
...mapState(['tags', 'filters', 'in_past', 'settings']), ...mapState(['settings']),
attributes () { attributes () {
return attributesFromEvents(this.events, this.tags) return attributesFromEvents(this.events)
} }
}, },
methods: { methods: {
...mapActions(['updateEvents', 'showPastEvents']),
updatePage (page) { updatePage (page) {
return new Promise((resolve, reject) => {
this.$emit('monthchange', page) this.$emit('monthchange', page)
})
}, },
click (day) { click (day) {
this.$emit('dayclick', day) this.$emit('dayclick', day)

45
components/Completed.vue Normal file
View File

@@ -0,0 +1,45 @@
<template lang="pug">
v-container
v-card-title.d-block.text-h5.text-center(v-text="$t('setup.completed')")
v-card-text(v-html="$t('setup.completed_description', user)")
v-alert.mb-3.mt-1(v-if='isHttp' outlined type='warning' color='red' show-icon :icon='mdiAlert') {{$t('setup.https_warning')}}
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(v-text='mdiArrowRight')
</template>
<script>
import { mdiArrowRight, mdiAlert } from '@mdi/js'
export default {
props: {
isHttp: { type: Boolean, default: false },
},
data () {
return {
mdiArrowRight, mdiAlert,
loading: false,
user: {
email: 'admin',
password: ''
}
}
},
methods: {
next () {
window.location='/admin'
},
async start (user) {
this.user = { ...user }
this.loading = true
try {
await this.$axios.$get('/ping')
this.loading = false
} catch (e) {
setTimeout(() => this.start(user), 1000)
}
}
}
}
</script>

View File

@@ -34,16 +34,54 @@ v-col(cols=12)
v-row.mt-3.col-md-6.mx-auto v-row.mt-3.col-md-6.mx-auto
v-col.col-12.col-sm-6 v-col.col-12.col-sm-6
v-select(dense :label="$t('event.from')" :value='fromHour' clearable v-menu(
v-model="menuFromHour"
:close-on-content-click="false"
offset-y
:value="fromHour"
transition="scale-transition")
template(v-slot:activator="{ on, attrs }")
v-text-field(
:label="$t('event.from')"
:value="fromHour"
:disabled='!value.from' :disabled='!value.from'
:prepend-icon="mdiClockTimeFourOutline"
:rules="[$validators.required('event.from')]" :rules="[$validators.required('event.from')]"
:items='hourList' @change='hr => change("fromHour", hr)') readonly
v-bind="attrs"
v-on="on")
v-time-picker(
v-if="menuFromHour"
:value="fromHour"
:allowedMinutes='allowedMinutes'
format='24hr'
@click:minute='menuFromHour=false'
@change='hr => change("fromHour", hr)')
v-col.col-12.col-sm-6 v-col.col-12.col-sm-6
v-select(dense :label="$t('event.due')" v-menu(
v-model="menuDueHour"
:close-on-content-click="false"
offset-y
:value="dueHour"
transition="scale-transition")
template(v-slot:activator="{ on, attrs }")
v-text-field(
:label="$t('event.due')"
:value="dueHour"
:disabled='!fromHour' :disabled='!fromHour'
:value='dueHour' clearable :prepend-icon="mdiClockTimeEightOutline"
:items='hourList' @change='hr => change("dueHour", hr)') readonly
v-bind="attrs"
v-on="on")
v-time-picker(
v-if="menuDueHour"
:value="dueHour"
:allowedMinutes='allowedMinutes'
format='24hr'
@click:minute='menuDueHour=false'
@change='hr => change("dueHour", hr)')
List(v-if='type==="normal" && todayEvents.length' :events='todayEvents' :title='$t("event.same_day")') List(v-if='type==="normal" && todayEvents.length' :events='todayEvents' :title='$t("event.same_day")')
@@ -52,7 +90,8 @@ v-col(cols=12)
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import List from '@/components/List' import List from '@/components/List'
import { attributesFromEvents } from '../../assets/helper' import { attributesFromEvents } from '../assets/helper'
import { mdiClockTimeFourOutline, mdiClockTimeEightOutline } from '@mdi/js'
export default { export default {
name: 'DateInput', name: 'DateInput',
@@ -63,6 +102,10 @@ export default {
}, },
data () { data () {
return { return {
mdiClockTimeFourOutline, mdiClockTimeEightOutline,
allowedMinutes: [0, 15, 30, 45],
menuFromHour: false,
menuDueHour: false,
type: 'normal', type: 'normal',
page: null, page: null,
events: [], events: [],
@@ -74,15 +117,14 @@ export default {
} }
}, },
computed: { computed: {
...mapState(['settings', 'tags']), ...mapState(['settings']),
todayEvents () { todayEvents () {
const start = dayjs(this.value.from).startOf('day').unix() const start = dayjs(this.value.from).startOf('day').unix()
const end = dayjs(this.value.from).endOf('day').unix() const end = dayjs(this.value.from).endOf('day').unix()
const events = this.events.filter(e => e.start_datetime >= start && e.start_datetime <= end) return this.events.filter(e => e.start_datetime >= start && e.start_datetime <= end)
return events
}, },
attributes () { attributes () {
return attributesFromEvents(this.events, this.tags) return attributesFromEvents(this.events)
}, },
fromDate () { fromDate () {
if (this.value.multidate) { if (this.value.multidate) {
@@ -92,21 +134,10 @@ export default {
}, },
fromHour () { fromHour () {
return this.value.from && this.value.fromHour ? dayjs(this.value.from).format('HH:mm') : null return this.value.from && this.value.fromHour ? dayjs.tz(this.value.from).format('HH:mm') : null
}, },
dueHour () { dueHour () {
return this.value.due && this.value.dueHour ? dayjs(this.value.due).format('HH:mm') : null return this.value.due && this.value.dueHour ? dayjs.tz(this.value.due).format('HH:mm') : null
},
hourList () {
const hourList = []
const leftPad = h => ('00' + h).slice(-2)
for (let h = 0; h < 24; h++) {
const textHour = leftPad(h < 13 ? h : h - 12)
hourList.push({ text: textHour + ':00 ' + (h <= 12 ? 'AM' : 'PM'), value: leftPad(h) + ':00' })
hourList.push({ text: textHour + ':30 ' + (h <= 12 ? 'AM' : 'PM'), value: leftPad(h) + ':30' })
}
return hourList
}, },
whenPatterns () { whenPatterns () {
if (!this.value.from) { return } if (!this.value.from) { return }
@@ -196,7 +227,7 @@ export default {
} else if (what === 'fromHour') { } else if (what === 'fromHour') {
if (value) { if (value) {
const [hour, minute] = value.split(':') const [hour, minute] = value.split(':')
const from = dayjs(this.value.from).hour(hour).minute(minute).second(0) const from = dayjs.tz(this.value.from).hour(hour).minute(minute).second(0)
this.$emit('input', { ...this.value, from, fromHour: true }) this.$emit('input', { ...this.value, from, fromHour: true })
} else { } else {
this.$emit('input', { ...this.value, fromHour: false }) this.$emit('input', { ...this.value, fromHour: false })
@@ -204,7 +235,7 @@ export default {
} else if (what === 'dueHour') { } else if (what === 'dueHour') {
if (value) { if (value) {
const [hour, minute] = value.split(':') const [hour, minute] = value.split(':')
const fromHour = dayjs(this.value.from).hour() const fromHour = dayjs.tz(this.value.from).hour()
// add a day // add a day
let due = dayjs(this.value.from) let due = dayjs(this.value.from)
@@ -226,20 +257,20 @@ export default {
let from = value.start let from = value.start
let due = value.end let due = value.end
if (this.value.fromHour) { if (this.value.fromHour) {
from = dayjs(value.start).hour(dayjs(this.value.from).hour()) from = dayjs.tz(value.start).hour(dayjs.tz(this.value.from).hour())
} }
if (this.value.dueHour) { if (this.value.dueHour) {
due = dayjs(value.end).hour(dayjs(this.value.due).hour()) due = dayjs.tz(value.end).hour(dayjs.tz(this.value.due).hour())
} }
this.$emit('input', { ...this.value, from, due }) this.$emit('input', { ...this.value, from, due })
} else { } else {
let from = value let from = value
let due = this.value.due let due = this.value.due
if (this.value.fromHour) { if (this.value.fromHour) {
from = dayjs(value).hour(dayjs(this.value.from).hour()) from = dayjs.tz(value).hour(dayjs.tz(this.value.from).hour())
} }
if (this.value.dueHour && this.value.due) { if (this.value.dueHour && this.value.due) {
due = dayjs(value).hour(dayjs(this.value.due).hour()) due = dayjs.tz(value).hour(dayjs.tz(this.value.due).hour())
} }
this.$emit('input', { ...this.value, from, due }) this.$emit('input', { ...this.value, from, due })
} }

View File

@@ -176,7 +176,7 @@ export default {
} }
} }
</script> </script>
<style lang='less'> <style lang='scss'>
.editor { .editor {
margin-top: 4px; margin-top: 4px;

View File

@@ -1,23 +1,23 @@
<template lang="pug"> <template lang="pug">
v-card.h-event.event.d-flex(itemscope itemtype="https://schema.org/Event") v-card.h-event.event.d-flex(itemscope itemtype="https://schema.org/Event")
nuxt-link(:to='`/event/${event.slug || event.id}`' itemprop="url") 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 }") MyPicture(:event='event' thumb :lazy='lazy')
v-icon.float-right.mr-1(v-if='event.parentId' color='success' v-text='mdiRepeat') v-icon.float-right.mr-1(v-if='event.parentId' color='success' v-text='mdiRepeat')
.title.p-name(itemprop="name") {{event.title}} .title.p-name(itemprop="name") {{event.title}}
v-card-text.body.pt-0.pb-0 v-card-text.body.pt-0.pb-0
time.dt-start.subtitle-1(:datetime='event.start_datetime|unixFormat("YYYY-MM-DD HH:mm")' itemprop="startDate" :content="event.start_datetime|unixFormat('YYYY-MM-DDTHH:mm')") <v-icon v-text='mdiCalendar'></v-icon> {{ event|when }} 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')}} .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}} nuxt-link.place.d-block.p-location.pl-0(text color='primary' :to='`/p/${event.place.name}`' itemprop="location" :content="event.place.name") <v-icon v-text='mdiMapMarker'></v-icon> {{event.place.name}}
.d-none(itemprop='location.address') {{event.place.address}} .d-none(itemprop='location.address') {{event.place.address}}
v-card-actions.pt-0.actions.justify-space-between v-card-actions.pt-0.actions.justify-space-between
.tags .tags
v-chip.ml-1.mt-1(v-for='tag in event.tags.slice(0,6)' small v-chip.ml-1.mt-1(v-for='tag in event.tags.slice(0,6)' small :to='`/tag/${tag}`'
:key='tag' outlined color='primary' @click="$emit('tagclick', tag)") {{tag}} :key='tag' outlined color='primary') {{tag}}
client-only client-only
v-menu(offset-y) v-menu(offset-y eager)
template(v-slot:activator="{on}") template(v-slot:activator="{on}")
v-btn.align-self-end(icon v-on='on' color='primary' title='more' aria-label='more') v-btn.align-self-end(icon v-on='on' color='primary' title='more' aria-label='more')
v-icon(v-text='mdiDotsVertical') v-icon(v-text='mdiDotsVertical')
@@ -50,6 +50,7 @@
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
import clipboard from '../assets/clipboard' import clipboard from '../assets/clipboard'
import MyPicture from '~/components/MyPicture'
import { mdiRepeat, mdiPencil, mdiDotsVertical, mdiContentCopy, import { mdiRepeat, mdiPencil, mdiDotsVertical, mdiContentCopy,
mdiCalendarExport, mdiDeleteForever, mdiCalendar, mdiMapMarker } from '@mdi/js' mdiCalendarExport, mdiDeleteForever, mdiCalendar, mdiMapMarker } from '@mdi/js'
@@ -58,6 +59,9 @@ export default {
return { mdiRepeat, mdiPencil, mdiDotsVertical, mdiContentCopy, mdiCalendarExport, return { mdiRepeat, mdiPencil, mdiDotsVertical, mdiContentCopy, mdiCalendarExport,
mdiDeleteForever, mdiMapMarker, mdiCalendar } mdiDeleteForever, mdiMapMarker, mdiCalendar }
}, },
components: {
MyPicture
},
props: { props: {
event: { type: Object, default: () => ({}) }, event: { type: Object, default: () => ({}) },
lazy: Boolean lazy: Boolean
@@ -65,25 +69,6 @@ export default {
mixins: [clipboard], mixins: [clipboard],
computed: { computed: {
...mapState(['settings']), ...mapState(['settings']),
thumbnail () {
let path
if (this.event.media && this.event.media.length) {
path = '/media/thumb/' + this.event.media[0].url
} else {
path = '/noimg.svg'
}
return path
},
alt () {
return this.event.media && this.event.media.length ? this.event.media[0].name : ''
},
thumbnailPosition () {
if (this.event.media && this.event.media.length && this.event.media[0].focalpoint) {
const focalpoint = this.event.media[0].focalpoint
return `${(focalpoint[0] + 1) * 50}% ${(focalpoint[1] + 1) * 50}%`
}
return 'center center'
},
is_mine () { is_mine () {
if (!this.$auth.user) { if (!this.$auth.user) {
return false return false
@@ -99,6 +84,8 @@ export default {
if (!ret) { return } if (!ret) { return }
await this.$axios.delete(`/event/${this.event.id}`) await this.$axios.delete(`/event/${this.event.id}`)
this.$emit('destroy', this.event.id) this.$emit('destroy', this.event.id)
this.$root.$message('admin.event_remove_ok')
} }
} }
} }

View File

@@ -45,7 +45,6 @@ export default {
event: {} event: {}
} }
}, },
computed: mapState(['places']),
methods: { methods: {
importGeneric () { importGeneric () {
if (this.file) { if (this.file) {

View File

@@ -61,12 +61,12 @@ export default {
} }
} }
</script> </script>
<style lang='less'> <style>
#list { #list {
max-width: 500px; max-width: 500px;
margin: 0 auto; margin: 0 auto;
.v-list-item__title { }
#list .v-list-item__title {
white-space: normal !important; white-space: normal !important;
} }
}
</style> </style>

View File

@@ -15,7 +15,7 @@
v-col.col-12.col-sm-4 v-col.col-12.col-sm-4
p {{$t('event.choose_focal_point')}} p {{$t('event.choose_focal_point')}}
img.img.d-none.d-sm-block(v-if='mediaPreview' img.mediaPreview.d-none.d-sm-block(v-if='mediaPreview'
:src='mediaPreview' :style="{ 'object-position': position }") :src='mediaPreview' :style="{ 'object-position': position }")
v-textarea.mt-4(type='text' v-textarea.mt-4(type='text'
@@ -35,7 +35,7 @@
v-btn(text color='primary' @click='openMediaDetails = true') {{$t('common.edit')}} v-btn(text color='primary' @click='openMediaDetails = true') {{$t('common.edit')}}
v-btn(text color='error' @click='remove') {{$t('common.remove')}} v-btn(text color='error' @click='remove') {{$t('common.remove')}}
div(v-if='mediaPreview') div(v-if='mediaPreview')
img.img.col-12.ml-3(:src='mediaPreview' :style="{ 'object-position': savedPosition }") img.mediaPreview.col-12.ml-3(:src='mediaPreview' :style="{ 'object-position': savedPosition }")
span.float-right {{event.media[0].name}} span.float-right {{event.media[0].name}}
v-file-input( v-file-input(
v-else v-else
@@ -53,7 +53,7 @@ export default {
name: 'MediaInput', name: 'MediaInput',
props: { props: {
value: { type: Object, default: () => ({ image: null }) }, value: { type: Object, default: () => ({ image: null }) },
event: { type: Object, default: () => {} } event: { type: Object, default: () => ({}) }
}, },
data () { data () {
return { return {
@@ -142,7 +142,7 @@ export default {
cursor: crosshair; cursor: crosshair;
} }
.img { .mediaPreview {
width: 100%; width: 100%;
object-fit: cover; object-fit: cover;
object-position: top; object-position: top;

97
components/MyPicture.vue Normal file
View File

@@ -0,0 +1,97 @@
<template>
<div :class='{ img: true, thumb }'>
<img
v-if='media'
:class='{ "u-featured": true }'
:alt='media.name' :loading='lazy?"lazy":"eager"'
:src="src"
:srcset="srcset"
itemprop="image"
:height="height" :width="width"
:style="{ 'object-position': thumbnailPosition }">
<img v-else-if='!media && thumb' class='thumb' src="/noimg.svg" alt=''>
</div>
</template>
<script>
export default {
props: {
event: { type: Object, default: () => ({}) },
thumb: { type: Boolean, default: false },
lazy: { type: Boolean, default: false },
showPreview: { type: Boolean, default: true }
},
computed: {
backgroundPreview () {
if (this.media && this.media.preview) {
return {
backgroundPosition: this.thumbnailPosition,
backgroundImage: "url('data:image/png;base64," + this.media.preview + "')" }
}
},
srcset () {
if (this.thumb) return ''
return `/media/thumb/${this.media.url} 500w, /media/${this.media.url} 1200w`
},
media () {
return this.event.media && this.event.media[0]
},
height () {
return this.media ? this.media.height : 'auto'
},
width () {
return this.media ? this.media.width : 'auto'
},
src () {
if (this.media) {
return '/media/thumb/' + this.media.url
}
if (this.thumb) {
return '/noimg.svg'
}
},
thumbnailPosition () {
if (this.media.focalpoint) {
const focalpoint = this.media.focalpoint
return `${(focalpoint[0] + 1) * 50}% ${(focalpoint[1] + 1) * 50}%`
}
return 'center center'
},
}
}
</script>
<style>
.img {
width: 100%;
height: auto;
position: relative;
overflow: hidden;
display: flex;
background-size: contain;
}
.img img {
object-fit: contain;
max-height: 125vh;
display: flex;
width: 100%;
max-width: 100%;
height: auto;
overflow: hidden;
transition: opacity .5s;
opacity: 1;
background-size: 100%;
}
.img.thumb img {
display: flex;
max-height: 250px;
min-height: 160px;
object-fit: cover;
object-position: top;
aspect-ratio: 1.7778;
}
</style>

View File

@@ -2,13 +2,13 @@
v-app-bar(app aria-label='Menu' height=64) v-app-bar(app aria-label='Menu' height=64)
//- logo, title and description //- logo, title and description
v-list-item(:to='$route.name==="index"?"/about":"/"') v-list-item.pa-0(:to='$route.name==="index"?"/about":"/"')
v-list-item-avatar(tile) v-list-item-avatar.ma-xs-1(tile)
v-img(src='/logo.png' alt='home') img(src='/logo.png' height='40')
v-list-item-content.d-none.d-sm-flex v-list-item-content.d-flex
v-list-item-title v-list-item-title.d-flex
h2 {{settings.title}} h2 {{settings.title}}
v-list-item-subtitle {{settings.description}} v-list-item-subtitle.d-none.d-sm-flex {{settings.description}}
v-spacer 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-btn(v-if='$auth.loggedIn || settings.allow_anon_event' icon nuxt to='/add' :aria-label='$t("common.add_event")' :title='$t("common.add_event")')
@@ -21,7 +21,7 @@
v-icon(v-text='mdiLogin') v-icon(v-text='mdiLogin')
client-only client-only
v-menu(v-if='$auth.loggedIn' offset-y) v-menu(v-if='$auth.loggedIn' offset-y eager)
template(v-slot:activator="{ on, attrs }") template(v-slot:activator="{ on, attrs }")
v-btn(icon v-bind='attrs' v-on='on' title='Menu' aria-label='Menu') v-btn(icon v-bind='attrs' v-on='on' title='Menu' aria-label='Menu')
v-icon(v-text='mdiDotsVertical') v-icon(v-text='mdiDotsVertical')
@@ -48,7 +48,7 @@
v-icon(v-text='mdiDotsVertical') v-icon(v-text='mdiDotsVertical')
v-btn(icon target='_blank' :href='feedLink' title='RSS' aria-label='RSS') v-btn(icon target='_blank' :href='`${settings.baseurl}/feed/rss`' title='RSS' aria-label='RSS')
v-icon(color='orange' v-text='mdiRss') v-icon(color='orange' v-text='mdiRss')
</template> </template>
@@ -64,25 +64,7 @@ export default {
return { mdiPlus, mdiShareVariant, mdiLogout, mdiLogin, mdiDotsVertical, mdiAccount, mdiCog, mdiRss } return { mdiPlus, mdiShareVariant, mdiLogout, mdiLogin, mdiDotsVertical, mdiAccount, mdiCog, mdiRss }
}, },
mixins: [clipboard], mixins: [clipboard],
computed: { computed: mapState(['settings']),
...mapState(['filters', 'settings']),
feedLink () {
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) {
query = '?'
if (tags) {
query += 'tags=' + tags
if (places) { query += '&places=' + places }
} else {
query += 'places=' + places
}
}
return `${this.settings.baseurl}/feed/rss${query}`
},
},
methods: { methods: {
logout () { logout () {
this.$root.$message('common.logout_ok') this.$root.$message('common.logout_ok')

View File

@@ -1,98 +1,89 @@
<template lang="pug"> <template lang="pug">
v-container.pt-0.pt-md-2 v-container.pt-0.pt-md-2
v-switch.mt-0( v-switch.mt-0(
v-if='recurrentFilter && settings.allow_recurrent_event' v-if='settings.allow_recurrent_event'
v-model='showRecurrent' v-model='showRecurrent'
inset color='primary' inset color='primary'
hide-details hide-details
:label="$t('event.show_recurrent')") :label="$t('event.show_recurrent')")
v-autocomplete( v-autocomplete(
v-model='meta'
:label='$t("common.search")' :label='$t("common.search")'
:items='keywords' :filter='filter'
cache-items
hide-details hide-details
color='primary'
hide-selected
small-chips
:items='items'
@change='change' @change='change'
:value='selectedFilters' hide-no-data
clearable @input.native='search'
:search-input.sync='search'
item-text='label' item-text='label'
return-object return-object
chips single-line chips
multiple) multiple)
template(v-slot:selection="data") template(v-slot:selection="{ attrs, item }")
v-chip(v-bind="data.attrs" v-chip(v-bind="attrs"
close close
:close-icon='mdiCloseCircle' @click:close='remove(item)'
@click:close='remove(data.item)' :close-icon='mdiCloseCircle')
:input-value="data.selected")
v-avatar(left) v-avatar(left)
v-icon(v-text="data.item.type === 'place' ? mdiMapMarker : mdiTag") v-icon(v-text="item.type === 'place' ? mdiMapMarker : mdiTag")
span {{ data.item.label }} span {{ item.label }}
template(v-slot:item='{ item }') template(v-slot:item='{ item }')
v-list-item-avatar v-list-item-avatar
v-icon(v-text="item.type === 'place' ? mdiMapMarker : mdiTag") v-icon(v-text="item.type === 'place' ? mdiMapMarker : mdiTag")
v-list-item-content v-list-item-content
v-list-item-title(v-text='item.label') v-list-item-title(v-text='item.label')
v-list-item-subtitle(v-if='item.type ==="place"' v-text='item.address')
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
import { mdiMapMarker, mdiTag, mdiCloseCircle } from '@mdi/js' import { mdiMapMarker, mdiTag, mdiCloseCircle } from '@mdi/js'
import debounce from 'lodash/debounce'
export default { export default {
name: 'Search', name: 'Search',
props: { props: {
recurrentFilter: { type: Boolean, default: true }, filters: { type: Object, default: () => ({}) }
filters: { type: Object, default: () => {} }
}, },
data () { data () {
return { return {
mdiTag, mdiMapMarker, mdiCloseCircle, mdiTag, mdiMapMarker, mdiCloseCircle,
tmpfilter: null, meta: [],
search: '' items: [],
} }
}, },
computed: { computed: {
...mapState(['tags', 'places', 'settings']), ...mapState(['settings']),
showRecurrent: { showRecurrent: {
get () { get () {
return this.filters.show_recurrent return this.filters.show_recurrent
}, },
set (v) { set (v) {
const filters = { this.change(v)
tags: this.filters.tags,
places: this.filters.places,
show_recurrent: v
}
this.$emit('update', filters)
} }
}, },
selectedFilters () {
const tags = this.tags.filter(t => this.filters.tags.includes(t.tag)).map(t => ({ type: 'tag', label: t.tag, weigth: t.weigth, id: t.tag }))
const places = this.places.filter(p => this.filters.places.includes(p.id))
.map(p => ({ type: 'place', label: p.name, weigth: p.weigth, id: p.id }))
const keywords = tags.concat(places).sort((a, b) => b.weigth - a.weigth)
return keywords
},
keywords () {
const tags = this.tags.map(t => ({ type: 'tag', label: t.tag, weigth: t.weigth, id: t.tag }))
const places = this.places.map(p => ({ type: 'place', label: p.name, weigth: p.weigth, id: p.id }))
const keywords = tags.concat(places).sort((a, b) => b.weigth - a.weigth)
return keywords
}
}, },
methods: { methods: {
remove (item) { filter (item, queryText, itemText) {
const filters = { return itemText.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1 ||
tags: item.type === 'tag' ? this.filters.tags.filter(f => f !== item.id) : this.filters.tags, item.address && item.address.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1
places: item.type === 'place' ? this.filters.places.filter(f => f !== item.id) : this.filters.places,
show_recurrent: this.filters.show_recurrent
}
this.$emit('update', filters)
}, },
change (filters) { search: debounce(async function(search) {
filters = { this.items = await this.$axios.$get(`/event/meta?search=${search.target.value}`)
tags: filters.filter(t => t.type === 'tag').map(t => t.id), }, 100),
places: filters.filter(p => p.type === 'place').map(p => p.id), remove (item) {
show_recurrent: this.filters.show_recurrent this.meta = this.meta.filter(m => m.type !== item.type || m.type === 'place' ? m.id !== item.id : m.tag !== item.tag)
this.change()
},
change (show_recurrent) {
const filters = {
tags: this.meta.filter(t => t.type === 'tag').map(t => t.label),
places: this.meta.filter(p => p.type === 'place').map(p => p.id),
show_recurrent: typeof show_recurrent !== 'undefined' ? show_recurrent : this.filters.show_recurrent
} }
this.$emit('update', filters) this.$emit('update', filters)
} }

115
components/WhereInput.vue Normal file
View File

@@ -0,0 +1,115 @@
<template lang="pug">
v-row
v-col(cols=12 md=6)
v-combobox(ref='place'
:rules="[$validators.required('common.where')]"
:label="$t('common.where')"
:hint="$t('event.where_description')"
:prepend-icon='mdiMapMarker'
no-filter
:value='value.name'
hide-no-data
@input.native='search'
persistent-hint
:items="places"
@change='selectPlace')
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' v-text='mdiPlus' :aria-label='$t("common.add")'></v-icon> {{item.name}}
v-list-item-content(two-line v-else)
v-list-item-title(v-text='item.name')
v-list-item-subtitle(v-text='item.address')
v-col(cols=12 md=6)
v-text-field(ref='address'
:prepend-icon='mdiMap'
:disabled='disableAddress'
:rules="[ v => disableAddress ? true : $validators.required('common.address')(v)]"
:label="$t('common.address')"
@change="changeAddress"
:value="value.address")
</template>
<script>
import { mdiMap, mdiMapMarker, mdiPlus } from '@mdi/js'
import debounce from 'lodash/debounce'
export default {
name: 'WhereInput',
props: {
value: { type: Object, default: () => ({}) }
},
data () {
return {
mdiMap, mdiMapMarker, mdiPlus,
place: { },
placeName: '',
places: [],
disableAddress: true
}
},
computed: {
filteredPlaces () {
if (!this.placeName) { return this.places }
const placeName = this.placeName.trim().toLowerCase()
let nameMatch = false
const matches = this.places.filter(p => {
const tmpName = p.name.toLowerCase()
const tmpAddress = p.address.toLowerCase()
if (tmpName.includes(placeName)) {
if (tmpName === placeName) { nameMatch = true }
return true
}
return tmpAddress.includes(placeName)
})
if (!nameMatch) {
matches.unshift({ create: true, name: this.placeName })
}
return matches
}
},
methods: {
search: debounce(async function(ev) {
const search = ev.target.value.trim().toLowerCase()
this.places = await this.$axios.$get(`place?search=${search}`)
if (!search) { return this.places }
const matches = this.places.find(p => search === p.name.toLocaleLowerCase())
if (!matches) {
this.places.unshift({ create: true, name: ev.target.value.trim() })
}
}, 100),
selectPlace (p) {
if (!p) { return }
if (typeof p === 'object' && !p.create) {
this.place.name = p.name.trim()
this.place.address = p.address
this.place.id = p.id
this.disableAddress = true
} else { // this is a new place
this.place.name = p.name || p
const tmpPlace = this.place.name.trim().toLocaleLowerCase()
// search for a place with the same name
const place = this.places.find(p => !p.create && p.name.trim().toLocaleLowerCase() === tmpPlace)
if (place) {
this.place.name = place.name
this.place.id = place.id
this.place.address = place.address
this.disableAddress = true
} else {
delete this.place.id
this.place.address = ''
this.disableAddress = false
this.$refs.place.blur()
this.$refs.address.focus()
}
}
this.$emit('input', { ...this.place })
},
changeAddress (v) {
this.place.address = v
this.$emit('input', { ...this.place })
}
}
}
</script>

View File

@@ -0,0 +1,207 @@
<template lang='pug'>
v-container
v-card-title {{$t('common.cohort')}}
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.cohort_description')")
v-btn(color='primary' text @click='newCohort') <v-icon v-text='mdiPlus'></v-icon> {{$t('common.new')}}
v-dialog(v-model='dialog' width='800' destroy-on-close :fullscreen='$vuetify.breakpoint.xsOnly')
v-card(color='secondary')
v-card-title {{$t('admin.edit_cohort')}}
v-card-text
v-form(v-model='valid' ref='form')
v-text-field(
v-if='!cohort.id'
:rules="[$validators.required('common.name')]"
:label="$t('common.name')"
v-model='cohort.name'
:placeholder='$t("common.name")')
template(v-slot:append-outer v-if='!cohort.id')
v-btn(text @click='saveCohort' color='primary' :loading='loading'
:disabled='!valid || loading || !!cohort.id') {{$t('common.save')}}
h3(v-else class='text-h5' v-text='cohort.name')
v-row
v-col(cols=5)
v-autocomplete(v-model='filterTags'
cache-items
:prepend-icon="mdiTagMultiple"
chips small-chips multiple deletable-chips hide-no-data hide-selected persistent-hint
:disabled="!cohort.id"
placeholder='Tutte'
@input.native='searchTags'
:delimiters="[',', ';']"
:items="tags"
:label="$t('common.tags')")
v-col(cols=5)
v-autocomplete(v-model='filterPlaces'
cache-items
:prepend-icon="mdiMapMarker"
chips small-chips multiple deletable-chips hide-no-data hide-selected persistent-hint
auto-select-first
clearable
return-object
item-text='name'
:disabled="!cohort.id"
@input.native="searchPlaces"
:delimiters="[',', ';']"
:items="places"
:label="$t('common.places')")
//- template(v-slot:item="{ item, attrs, on }")
//- v-list-item(v-bind='attrs' v-on='on')
//- v-list-item-content(two-line)
//- v-list-item-title(v-text='item.name')
//- v-list-item-subtitle(v-text='item.address')
v-col(cols=2)
v-btn(color='primary' text @click='addFilter' :disabled='!cohort.id || !filterPlaces.length && !filterTags.length') add <v-icon v-text='mdiPlus'></v-icon>
v-data-table(
:headers='filterHeaders'
:items='filters'
:hide-default-footer='filters.length<5'
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }')
template(v-slot:item.actions='{item}')
v-btn(@click='removeFilter(item)' color='error' icon)
v-icon(v-text='mdiDeleteForever')
template(v-slot:item.tags='{item}')
v-chip.ma-1(small v-for='tag in item.tags' v-text='tag' :key='tag')
template(v-slot:item.places='{item}')
v-chip.ma-1(small v-for='place in item.places' v-text='place.name' :key='place.id' )
v-card-actions
v-spacer
v-btn(text @click='dialog=false' color='warning') {{$t('common.close')}}
//- v-btn(text @click='saveCohort' color='primary' :loading='loading'
//- :disable='!valid || loading') {{$t('common.save')}}
v-card-text
v-data-table(
:headers='cohortHeaders'
:items='cohorts'
:hide-default-footer='cohorts.length<5'
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
:search='search')
template(v-slot:item.filters='{item}')
span {{cohortFilters(item)}}
template(v-slot:item.actions='{item}')
v-btn(@click='editCohort(item)' color='primary' icon)
v-icon(v-text='mdiPencil')
v-btn(@click='removeCohort(item)' color='error' icon)
v-icon(v-text='mdiDeleteForever')
</template>
<script>
import get from 'lodash/get'
import debounce from 'lodash/debounce'
import { mdiPencil, mdiChevronLeft, mdiChevronRight, mdiMagnify, mdiPlus, mdiTagMultiple, mdiMapMarker, mdiDeleteForever, mdiCloseCircle } from '@mdi/js'
export default {
data () {
return {
mdiPencil, mdiChevronRight, mdiChevronLeft, mdiMagnify, mdiPlus, mdiTagMultiple, mdiMapMarker, mdiDeleteForever, mdiCloseCircle,
loading: false,
dialog: false,
valid: false,
search: '',
cohort: { name: '', id: null },
filterTags: [],
filterPlaces: [],
tags: [],
places: [],
cohorts: [],
filters: [],
tagName: '',
placeName: '',
cohortHeaders: [
{ value: 'name', text: 'Name' },
{ value: 'filters', text: 'Filters' },
{ value: 'actions', text: 'Actions', align: 'right' }
],
filterHeaders: [
{ value: 'tags', text: 'Tags' },
{ value: 'places', text: 'Places' },
{ value: 'actions', text: 'Actions', align: 'right' }
]
}
},
async fetch () {
this.cohorts = await this.$axios.$get('/cohorts?withFilters=true')
},
methods: {
searchTags: debounce(async function (ev) {
this.tags = await this.$axios.$get(`/tag?search=${ev.target.value}`)
}, 100),
searchPlaces: debounce(async function (ev) {
this.places = await this.$axios.$get(`/place?search=${ev.target.value}`)
}, 100),
cohortFilters (cohort) {
return cohort.filters.map(f => {
return '(' + f.tags?.join(', ') + f.places?.map(p => p.name).join(', ') + ')'
}).join(' - ')
},
async addFilter () {
this.loading = true
const tags = this.filterTags
const places = this.filterPlaces.map(p => ({ id: p.id, name: p.name }))
const filter = await this.$axios.$post('/filter', { cohortId: this.cohort.id, tags, places })
this.$fetch()
this.filters.push(filter)
this.filterTags = []
this.filterPlaces = []
this.loading = false
},
async editCohort (cohort) {
this.cohort = { ...cohort }
this.filters = await this.$axios.$get(`/filter/${cohort.id}`)
this.dialog = true
},
newCohort () {
this.cohort = { name: '', id: null },
this.filters = []
this.dialog = true
},
async saveCohort () {
if (!this.$refs.form.validate()) return
this.loading = true
this.cohort = await this.$axios.$post('/cohorts', this.cohort)
this.$fetch()
this.loading = false
},
async removeFilter(filter) {
try {
await this.$axios.$delete(`/filter/${filter.id}`)
this.filters = this.filters.filter(f => f.id !== filter.id)
this.$fetch()
} catch (e) {
const err = get(e, 'response.data.errors[0].message', e)
this.$root.$message(this.$t(err), { color: 'error' })
this.loading = false
}
},
async removeCohort (cohort) {
const ret = await this.$root.$confirm('admin.delete_cohort_confirm', { cohort: cohort.name })
if (!ret) { return }
try {
await this.$axios.$delete(`/cohort/${cohort.id}`)
this.cohorts = this.cohorts.filter(c => c.id !== cohort.id)
} catch (e) {
const err = get(e, 'response.data.errors[0].message', e)
this.$root.$message(this.$t(err), { color: 'error' })
this.loading = false
}
}
}
}
</script>

View File

@@ -8,6 +8,7 @@
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }' :footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
:items='unconfirmedEvents' :items='unconfirmedEvents'
:headers='headers') :headers='headers')
template(v-slot:item.when='{ item }') {{item|when}}
template(v-slot:item.actions='{ item }') template(v-slot:item.actions='{ item }')
v-btn(text small @click='confirm(item)' color='success') {{$t('common.confirm')}} v-btn(text small @click='confirm(item)' color='success') {{$t('common.confirm')}}
v-btn(text small :to='`/event/${item.slug || item.id}`' color='success') {{$t('common.preview')}} v-btn(text small :to='`/event/${item.slug || item.id}`' color='success') {{$t('common.preview')}}
@@ -31,6 +32,8 @@ export default {
editing: false, editing: false,
headers: [ headers: [
{ value: 'title', text: 'Title' }, { value: 'title', text: 'Title' },
{ value: 'place.name', text: 'Place' },
{ value: 'when', text: 'When' },
{ value: 'actions', text: 'Actions', align: 'right' } { value: 'actions', text: 'Actions', align: 'right' }
] ]
} }

View File

@@ -126,6 +126,7 @@ export default {
if (!this.instance_url.startsWith('http')) { if (!this.instance_url.startsWith('http')) {
this.instance_url = `https://${this.instance_url}` this.instance_url = `https://${this.instance_url}`
} }
this.instance_url = this.instance_url.replace(/\/$/, '')
const instance = await axios.get(`${this.instance_url}/.well-known/nodeinfo/2.1`) const instance = await axios.get(`${this.instance_url}/.well-known/nodeinfo/2.1`)
this.setSetting({ this.setSetting({
key: 'trusted_instances', key: 'trusted_instances',

View File

@@ -41,18 +41,21 @@
template(v-slot:item.actions='{item}') template(v-slot:item.actions='{item}')
v-btn(@click='editPlace(item)' color='primary' icon) v-btn(@click='editPlace(item)' color='primary' icon)
v-icon(v-text='mdiPencil') v-icon(v-text='mdiPencil')
nuxt-link(:to='`/p/${item.name}`')
v-icon(v-text='mdiEye')
</template> </template>
<script> <script>
import { mapState, mapActions } from 'vuex' import { mdiPencil, mdiChevronLeft, mdiChevronRight, mdiMagnify, mdiEye } from '@mdi/js'
import { mdiPencil, mdiChevronLeft, mdiChevronRight } from '@mdi/js'
export default { export default {
data () { data () {
return { return {
mdiPencil, mdiChevronRight, mdiChevronLeft, mdiPencil, mdiChevronRight, mdiChevronLeft, mdiMagnify, mdiEye,
loading: false, loading: false,
dialog: false, dialog: false,
valid: false, valid: false,
places: [],
search: '', search: '',
place: { name: '', address: '', id: null }, place: { name: '', address: '', id: null },
headers: [ headers: [
@@ -62,9 +65,10 @@ export default {
] ]
} }
}, },
computed: mapState(['places']), async fetch () {
this.places = await this.$axios.$get('/place/all')
},
methods: { methods: {
...mapActions(['updateMeta']),
editPlace (item) { editPlace (item) {
this.place.name = item.name this.place.name = item.name
this.place.address = item.address this.place.address = item.address

View File

@@ -9,7 +9,7 @@
accept='image/*') accept='image/*')
template(slot='append-outer') template(slot='append-outer')
v-btn(color='warning' text @click='resetLogo') <v-icon v-text='mdiRestore'></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}`' v-img(:src='`/logo.png?${logoKey}`'
max-width="60px" max-height="60px" contain) max-width="60px" max-height="60px" contain)
v-switch.mt-5(v-model='is_dark' v-switch.mt-5(v-model='is_dark'
@@ -54,25 +54,27 @@
v-btn(color='warning' text @click='reset') <v-icon v-text='mdiRestore'></v-icon> {{$t('common.reset')}} v-btn(color='warning' text @click='reset') <v-icon v-text='mdiRestore'></v-icon> {{$t('common.reset')}}
v-card v-card
v-list.mt-1(two-line subheader) v-list.mt-1(two-line subheader)
v-list-item(v-for='link in settings.footerLinks' v-list-item(v-for='(link, idx) in settings.footerLinks'
:key='`${link.label}`' @click='editFooterLink(link)') :key='`${link.label}`' @click='editFooterLink(link)')
v-list-item-content v-list-item-content
v-list-item-title {{link.label}} v-list-item-title {{link.label}}
v-list-item-subtitle {{link.href}} v-list-item-subtitle {{link.href}}
v-list-item-action v-list-item-action
v-btn(icon color='error' @click.stop='removeFooterLink(link)') v-btn.left(v-if='idx !== 0' icon color='warn' @click.stop='moveUpFooterLink(link, idx)')
v-icon(v-text='mdiChevronUp')
v-btn.float-right(icon color='error' @click.stop='removeFooterLink(link)')
v-icon(v-text='mdiDeleteForever') v-icon(v-text='mdiDeleteForever')
</template> </template>
<script> <script>
import { mapActions, mapState } from 'vuex' import { mapActions, mapState } from 'vuex'
import { mdiDeleteForever, mdiRestore, mdiPlus } from '@mdi/js' import { mdiDeleteForever, mdiRestore, mdiPlus, mdiChevronUp } from '@mdi/js'
export default { export default {
name: 'Theme', name: 'Theme',
data () { data () {
return { return {
mdiDeleteForever, mdiRestore, mdiPlus, mdiDeleteForever, mdiRestore, mdiPlus, mdiChevronUp,
valid: false, valid: false,
logoKey: 0, logoKey: 0,
link: { href: '', label: '' }, link: { href: '', label: '' },
@@ -152,6 +154,12 @@ export default {
const footerLinks = this.settings.footerLinks.filter(l => l.label !== item.label) const footerLinks = this.settings.footerLinks.filter(l => l.label !== item.label)
this.setSetting({ key: 'footerLinks', value: footerLinks }) this.setSetting({ key: 'footerLinks', value: footerLinks })
}, },
async moveUpFooterLink (item, idx) {
const footerLinks = [...this.settings.footerLinks]
footerLinks[idx] = footerLinks[idx-1]
footerLinks[idx-1] = this.settings.footerLinks[idx]
this.setSetting({ key: 'footerLinks', value: footerLinks })
},
editFooterLink (item) { editFooterLink (item) {
this.link = { href: item.href, label: item.label } this.link = { href: item.href, label: item.label }
this.linkModal = true this.linkModal = true

View File

@@ -16,7 +16,7 @@ v-card
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
import clipboard from '../../assets/clipboard' import clipboard from '../assets/clipboard'
import { mdiContentCopy, mdiInformation } from '@mdi/js' import { mdiContentCopy, mdiInformation } from '@mdi/js'
export default { export default {

View File

@@ -37,3 +37,4 @@ end
# Performance-booster for watching directories on Windows # Performance-booster for watching directories on Windows
gem "wdm", "~> 0.1.0", :install_if => Gem.win_platform? gem "wdm", "~> 0.1.0", :install_if => Gem.win_platform?
gem "webrick", "~> 1.7"

View File

@@ -1,7 +1,7 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
activesupport (6.0.4.7) activesupport (6.0.4.8)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
minitest (~> 5.1) minitest (~> 5.1)
@@ -10,7 +10,7 @@ GEM
addressable (2.8.0) addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0) public_suffix (>= 2.0.2, < 5.0)
colorator (1.1.0) colorator (1.1.0)
concurrent-ruby (1.1.9) concurrent-ruby (1.1.10)
em-websocket (0.5.3) em-websocket (0.5.3)
eventmachine (>= 0.12.9) eventmachine (>= 0.12.9)
http_parser.rb (~> 0) http_parser.rb (~> 0)
@@ -18,7 +18,7 @@ GEM
ffi (1.15.5) ffi (1.15.5)
forwardable-extended (2.6.0) forwardable-extended (2.6.0)
gemoji (3.0.1) gemoji (3.0.1)
html-pipeline (2.14.0) html-pipeline (2.14.1)
activesupport (>= 2) activesupport (>= 2)
nokogiri (>= 1.4) nokogiri (>= 1.4)
http_parser.rb (0.8.0) http_parser.rb (0.8.0)
@@ -57,7 +57,7 @@ GEM
jekyll (>= 3.8.5) jekyll (>= 3.8.5)
jekyll-seo-tag (~> 2.0) jekyll-seo-tag (~> 2.0)
rake (>= 12.3.1, < 13.1.0) rake (>= 12.3.1, < 13.1.0)
kramdown (2.3.1) kramdown (2.4.0)
rexml rexml
kramdown-parser-gfm (1.1.0) kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0) kramdown (~> 2.0)
@@ -68,13 +68,13 @@ GEM
mercenary (0.4.0) mercenary (0.4.0)
mini_magick (4.11.0) mini_magick (4.11.0)
minitest (5.15.0) minitest (5.15.0)
nokogiri (1.13.3-x86_64-linux) nokogiri (1.13.6-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
pathutil (0.16.2) pathutil (0.16.2)
forwardable-extended (~> 2.6) forwardable-extended (~> 2.6)
premonition (4.0.2) premonition (4.0.2)
jekyll (>= 3.7, < 5.0) jekyll (>= 3.7, < 5.0)
public_suffix (4.0.6) public_suffix (4.0.7)
racc (1.6.0) racc (1.6.0)
rake (13.0.6) rake (13.0.6)
rb-fsevent (0.11.1) rb-fsevent (0.11.1)
@@ -90,10 +90,11 @@ GEM
thread_safe (0.3.6) thread_safe (0.3.6)
tzinfo (1.2.9) tzinfo (1.2.9)
thread_safe (~> 0.1) thread_safe (~> 0.1)
tzinfo-data (1.2021.5) tzinfo-data (1.2022.1)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
unicode-display_width (1.8.0) unicode-display_width (1.8.0)
wdm (0.1.1) wdm (0.1.1)
webrick (1.7.0)
zeitwerk (2.5.4) zeitwerk (2.5.4)
PLATFORMS PLATFORMS
@@ -110,6 +111,7 @@ DEPENDENCIES
tzinfo (~> 1.2) tzinfo (~> 1.2)
tzinfo-data tzinfo-data
wdm (~> 0.1.0) wdm (~> 0.1.0)
webrick (~> 1.7)
BUNDLED WITH BUNDLED WITH
2.2.27 2.2.27

View File

@@ -1,16 +1,38 @@
--- ---
layout: default layout: default
title: Admin title: Admin
permalink: /admin permalink: /usage/admin
nav_order: 5 nav_order: 1
parent: Usage
--- ---
# Admin ### CLI
{: .no_toc }
#### Manage accounts
```bash
$ gancio accounts
Manage accounts
Commands:
gancio accounts list List all accounts
```
#### List accounts
```
$ gancio accounts list
📅 gancio - v1.4.3 - A shared agenda for local communities (nodejs: v16.13.0)
> Reading configuration from: ./config.json
1 admin: true enabled: true email: admin
2 admin: false enabled: true email: lesion@autistici.org
```
1. TOC
{:toc}
## Basics ## Basics
## Add user ## Add user

View File

@@ -7,6 +7,12 @@
<meta name="Description" content="{{ page.description }}"> <meta name="Description" content="{{ page.description }}">
{% endif %} {% endif %}
<link href="https://github.com/lesion" rel="me">
<link href="eventi@cisti.org" rel="me">
<link rel="webmention" href="https://webmention.io/gancio.org/webmention" />
<link rel="pingback" href="https://webmention.io/gancio.org/xmlrpc" />
<link rel="shortcut icon" href="{{ '/favicon.ico' | absolute_url }}" type="image/x-icon"> <link rel="shortcut icon" href="{{ '/favicon.ico' | absolute_url }}" type="image/x-icon">
<link rel="stylesheet" href="{{ '/assets/css/just-the-docs-default.css' | absolute_url }}"> <link rel="stylesheet" href="{{ '/assets/css/just-the-docs-default.css' | absolute_url }}">
<link rel="stylesheet" href="{{ '/assets/css/premonition.css' | absolute_url }}"> <link rel="stylesheet" href="{{ '/assets/css/premonition.css' | absolute_url }}">

View File

@@ -4,7 +4,7 @@ function run(fn) {
return fn(); return fn();
} }
function blank_object() { function blank_object() {
return Object.create(null); return /* @__PURE__ */ Object.create(null);
} }
function run_all(fns) { function run_all(fns) {
fns.forEach(run); fns.forEach(run);
@@ -104,7 +104,7 @@ function schedule_update() {
function add_render_callback(fn) { function add_render_callback(fn) {
render_callbacks.push(fn); render_callbacks.push(fn);
} }
const seen_callbacks = new Set(); const seen_callbacks = /* @__PURE__ */ new Set();
let flushidx = 0; let flushidx = 0;
function flush() { function flush() {
const saved_component = current_component; const saved_component = current_component;
@@ -146,7 +146,7 @@ function update($$) {
$$.after_update.forEach(add_render_callback); $$.after_update.forEach(add_render_callback);
} }
} }
const outroing = new Set(); const outroing = /* @__PURE__ */ new Set();
function transition_in(block, local) { function transition_in(block, local) {
if (block && block.i) { if (block && block.i) {
outroing.delete(block); outroing.delete(block);
@@ -282,19 +282,41 @@ if (typeof HTMLElement === "function") {
} }
function get_each_context(ctx, list, i) { function get_each_context(ctx, list, i) {
const child_ctx = ctx.slice(); const child_ctx = ctx.slice();
child_ctx[11] = list[i]; child_ctx[12] = list[i];
return child_ctx; return child_ctx;
} }
function get_each_context_1(ctx, list, i) { function get_each_context_1(ctx, list, i) {
const child_ctx = ctx.slice(); const child_ctx = ctx.slice();
child_ctx[14] = list[i]; child_ctx[15] = list[i];
return child_ctx; return child_ctx;
} }
function create_if_block_5(ctx) {
let link;
return {
c() {
link = element("link");
attr(link, "rel", "stylesheet");
attr(link, "href", ctx[4]);
},
m(target, anchor) {
insert(target, link, anchor);
},
p(ctx2, dirty) {
if (dirty & 16) {
attr(link, "href", ctx2[4]);
}
},
d(detaching) {
if (detaching)
detach(link);
}
};
}
function create_if_block$1(ctx) { function create_if_block$1(ctx) {
let div; let div;
let t; let t;
let if_block = ctx[1] && ctx[3] === "true" && create_if_block_4(ctx); let if_block = ctx[1] && ctx[3] === "true" && create_if_block_4(ctx);
let each_value = ctx[4]; let each_value = ctx[5];
let each_blocks = []; let each_blocks = [];
for (let i = 0; i < each_value.length; i += 1) { for (let i = 0; i < each_value.length; i += 1) {
each_blocks[i] = create_each_block(get_each_context(ctx, each_value, i)); each_blocks[i] = create_each_block(get_each_context(ctx, each_value, i));
@@ -336,8 +358,8 @@ function create_if_block$1(ctx) {
if_block.d(1); if_block.d(1);
if_block = null; if_block = null;
} }
if (dirty & 25) { if (dirty & 41) {
each_value = ctx2[4]; each_value = ctx2[5];
let i; let i;
for (i = 0; i < each_value.length; i += 1) { for (i = 0; i < each_value.length; i += 1) {
const child_ctx = get_each_context(ctx2, each_value, i); const child_ctx = get_each_context(ctx2, each_value, i);
@@ -395,7 +417,7 @@ function create_if_block_4(ctx) {
attr(div0, "class", "title"); attr(div0, "class", "title");
attr(img, "id", "logo"); attr(img, "id", "logo");
attr(img, "alt", "logo"); attr(img, "alt", "logo");
if (!src_url_equal(img.src, img_src_value = "" + (ctx[0] + "/logo.png"))) if (!src_url_equal(img.src, img_src_value = ctx[0] + "/logo.png"))
attr(img, "src", img_src_value); attr(img, "src", img_src_value);
attr(div1, "class", "content"); attr(div1, "class", "content");
attr(a, "href", ctx[0]); attr(a, "href", ctx[0]);
@@ -413,7 +435,7 @@ function create_if_block_4(ctx) {
p(ctx2, dirty) { p(ctx2, dirty) {
if (dirty & 2) if (dirty & 2)
set_data(t0, ctx2[1]); set_data(t0, ctx2[1]);
if (dirty & 1 && !src_url_equal(img.src, img_src_value = "" + (ctx2[0] + "/logo.png"))) { if (dirty & 1 && !src_url_equal(img.src, img_src_value = ctx2[0] + "/logo.png")) {
attr(img, "src", img_src_value); attr(img, "src", img_src_value);
} }
if (dirty & 1) { if (dirty & 1) {
@@ -429,7 +451,7 @@ function create_if_block_4(ctx) {
function create_if_block_2(ctx) { function create_if_block_2(ctx) {
let div; let div;
function select_block_type(ctx2, dirty) { function select_block_type(ctx2, dirty) {
if (ctx2[11].media.length) if (ctx2[12].media.length)
return create_if_block_3; return create_if_block_3;
return create_else_block; return create_else_block;
} }
@@ -472,7 +494,7 @@ function create_else_block(ctx) {
c() { c() {
img = element("img"); img = element("img");
attr(img, "style", "aspect-ratio=1.7778;"); attr(img, "style", "aspect-ratio=1.7778;");
attr(img, "alt", img_alt_value = ctx[11].title); attr(img, "alt", img_alt_value = ctx[12].title);
if (!src_url_equal(img.src, img_src_value = ctx[0] + "/noimg.svg")) if (!src_url_equal(img.src, img_src_value = ctx[0] + "/noimg.svg"))
attr(img, "src", img_src_value); attr(img, "src", img_src_value);
attr(img, "loading", "lazy"); attr(img, "loading", "lazy");
@@ -481,7 +503,7 @@ function create_else_block(ctx) {
insert(target, img, anchor); insert(target, img, anchor);
}, },
p(ctx2, dirty) { p(ctx2, dirty) {
if (dirty & 16 && img_alt_value !== (img_alt_value = ctx2[11].title)) { if (dirty & 32 && img_alt_value !== (img_alt_value = ctx2[12].title)) {
attr(img, "alt", img_alt_value); attr(img, "alt", img_alt_value);
} }
if (dirty & 1 && !src_url_equal(img.src, img_src_value = ctx2[0] + "/noimg.svg")) { if (dirty & 1 && !src_url_equal(img.src, img_src_value = ctx2[0] + "/noimg.svg")) {
@@ -502,9 +524,9 @@ function create_if_block_3(ctx) {
return { return {
c() { c() {
img = element("img"); img = element("img");
attr(img, "style", img_style_value = "object-position: " + position$1(ctx[11]) + "; aspect-ratio=1.7778;"); attr(img, "style", img_style_value = "object-position: " + position$1(ctx[12]) + "; aspect-ratio=1.7778;");
attr(img, "alt", img_alt_value = ctx[11].media[0].name); attr(img, "alt", img_alt_value = ctx[12].media[0].name);
if (!src_url_equal(img.src, img_src_value = ctx[0] + "/media/thumb/" + ctx[11].media[0].url)) if (!src_url_equal(img.src, img_src_value = ctx[0] + "/media/thumb/" + ctx[12].media[0].url))
attr(img, "src", img_src_value); attr(img, "src", img_src_value);
attr(img, "loading", "lazy"); attr(img, "loading", "lazy");
}, },
@@ -512,13 +534,13 @@ function create_if_block_3(ctx) {
insert(target, img, anchor); insert(target, img, anchor);
}, },
p(ctx2, dirty) { p(ctx2, dirty) {
if (dirty & 16 && img_style_value !== (img_style_value = "object-position: " + position$1(ctx2[11]) + "; aspect-ratio=1.7778;")) { if (dirty & 32 && img_style_value !== (img_style_value = "object-position: " + position$1(ctx2[12]) + "; aspect-ratio=1.7778;")) {
attr(img, "style", img_style_value); attr(img, "style", img_style_value);
} }
if (dirty & 16 && img_alt_value !== (img_alt_value = ctx2[11].media[0].name)) { if (dirty & 32 && img_alt_value !== (img_alt_value = ctx2[12].media[0].name)) {
attr(img, "alt", img_alt_value); attr(img, "alt", img_alt_value);
} }
if (dirty & 17 && !src_url_equal(img.src, img_src_value = ctx2[0] + "/media/thumb/" + ctx2[11].media[0].url)) { if (dirty & 33 && !src_url_equal(img.src, img_src_value = ctx2[0] + "/media/thumb/" + ctx2[12].media[0].url)) {
attr(img, "src", img_src_value); attr(img, "src", img_src_value);
} }
}, },
@@ -530,7 +552,7 @@ function create_if_block_3(ctx) {
} }
function create_if_block_1$1(ctx) { function create_if_block_1$1(ctx) {
let div; let div;
let each_value_1 = ctx[11].tags; let each_value_1 = ctx[12].tags;
let each_blocks = []; let each_blocks = [];
for (let i = 0; i < each_value_1.length; i += 1) { for (let i = 0; i < each_value_1.length; i += 1) {
each_blocks[i] = create_each_block_1(get_each_context_1(ctx, each_value_1, i)); each_blocks[i] = create_each_block_1(get_each_context_1(ctx, each_value_1, i));
@@ -550,8 +572,8 @@ function create_if_block_1$1(ctx) {
} }
}, },
p(ctx2, dirty) { p(ctx2, dirty) {
if (dirty & 16) { if (dirty & 32) {
each_value_1 = ctx2[11].tags; each_value_1 = ctx2[12].tags;
let i; let i;
for (i = 0; i < each_value_1.length; i += 1) { for (i = 0; i < each_value_1.length; i += 1) {
const child_ctx = get_each_context_1(ctx2, each_value_1, i); const child_ctx = get_each_context_1(ctx2, each_value_1, i);
@@ -579,7 +601,7 @@ function create_if_block_1$1(ctx) {
function create_each_block_1(ctx) { function create_each_block_1(ctx) {
let span; let span;
let t0; let t0;
let t1_value = ctx[14] + ""; let t1_value = ctx[15] + "";
let t1; let t1;
return { return {
c() { c() {
@@ -594,7 +616,7 @@ function create_each_block_1(ctx) {
append(span, t1); append(span, t1);
}, },
p(ctx2, dirty) { p(ctx2, dirty) {
if (dirty & 16 && t1_value !== (t1_value = ctx2[14] + "")) if (dirty & 32 && t1_value !== (t1_value = ctx2[15] + ""))
set_data(t1, t1_value); set_data(t1, t1_value);
}, },
d(detaching) { d(detaching) {
@@ -608,27 +630,27 @@ function create_each_block(ctx) {
let t0; let t0;
let div2; let div2;
let div0; let div0;
let t1_value = when$1(ctx[11].start_datetime) + ""; let t1_value = when$1(ctx[12].start_datetime) + "";
let t1; let t1;
let t2; let t2;
let div1; let div1;
let t3_value = ctx[11].title + ""; let t3_value = ctx[12].title + "";
let t3; let t3;
let t4; let t4;
let span1; let span1;
let t5; let t5;
let t6_value = ctx[11].place.name + ""; let t6_value = ctx[12].place.name + "";
let t6; let t6;
let t7; let t7;
let span0; let span0;
let t8_value = ctx[11].place.address + ""; let t8_value = ctx[12].place.address + "";
let t8; let t8;
let t9; let t9;
let t10; let t10;
let a_href_value; let a_href_value;
let a_title_value; let a_title_value;
let if_block0 = ctx[3] !== "true" && create_if_block_2(ctx); let if_block0 = ctx[3] !== "true" && create_if_block_2(ctx);
let if_block1 = ctx[11].tags.length && create_if_block_1$1(ctx); let if_block1 = ctx[12].tags.length && create_if_block_1$1(ctx);
return { return {
c() { c() {
a = element("a"); a = element("a");
@@ -657,9 +679,9 @@ function create_each_block(ctx) {
attr(span0, "class", "subtitle"); attr(span0, "class", "subtitle");
attr(span1, "class", "place"); attr(span1, "class", "place");
attr(div2, "class", "content"); attr(div2, "class", "content");
attr(a, "href", a_href_value = "" + (ctx[0] + "/event/" + (ctx[11].slug || ctx[11].id))); attr(a, "href", a_href_value = ctx[0] + "/event/" + (ctx[12].slug || ctx[12].id));
attr(a, "class", "event"); attr(a, "class", "event");
attr(a, "title", a_title_value = ctx[11].title); attr(a, "title", a_title_value = ctx[12].title);
attr(a, "target", "_blank"); attr(a, "target", "_blank");
}, },
m(target, anchor) { m(target, anchor) {
@@ -698,15 +720,15 @@ function create_each_block(ctx) {
if_block0.d(1); if_block0.d(1);
if_block0 = null; if_block0 = null;
} }
if (dirty & 16 && t1_value !== (t1_value = when$1(ctx2[11].start_datetime) + "")) if (dirty & 32 && t1_value !== (t1_value = when$1(ctx2[12].start_datetime) + ""))
set_data(t1, t1_value); set_data(t1, t1_value);
if (dirty & 16 && t3_value !== (t3_value = ctx2[11].title + "")) if (dirty & 32 && t3_value !== (t3_value = ctx2[12].title + ""))
set_data(t3, t3_value); set_data(t3, t3_value);
if (dirty & 16 && t6_value !== (t6_value = ctx2[11].place.name + "")) if (dirty & 32 && t6_value !== (t6_value = ctx2[12].place.name + ""))
set_data(t6, t6_value); set_data(t6, t6_value);
if (dirty & 16 && t8_value !== (t8_value = ctx2[11].place.address + "")) if (dirty & 32 && t8_value !== (t8_value = ctx2[12].place.address + ""))
set_data(t8, t8_value); set_data(t8, t8_value);
if (ctx2[11].tags.length) { if (ctx2[12].tags.length) {
if (if_block1) { if (if_block1) {
if_block1.p(ctx2, dirty); if_block1.p(ctx2, dirty);
} else { } else {
@@ -718,10 +740,10 @@ function create_each_block(ctx) {
if_block1.d(1); if_block1.d(1);
if_block1 = null; if_block1 = null;
} }
if (dirty & 17 && a_href_value !== (a_href_value = "" + (ctx2[0] + "/event/" + (ctx2[11].slug || ctx2[11].id)))) { if (dirty & 33 && a_href_value !== (a_href_value = ctx2[0] + "/event/" + (ctx2[12].slug || ctx2[12].id))) {
attr(a, "href", a_href_value); attr(a, "href", a_href_value);
} }
if (dirty & 16 && a_title_value !== (a_title_value = ctx2[11].title)) { if (dirty & 32 && a_title_value !== (a_title_value = ctx2[12].title)) {
attr(a, "title", a_title_value); attr(a, "title", a_title_value);
} }
}, },
@@ -736,41 +758,65 @@ function create_each_block(ctx) {
}; };
} }
function create_fragment$1(ctx) { function create_fragment$1(ctx) {
let if_block_anchor; let t;
let if_block = ctx[4].length && create_if_block$1(ctx); let if_block1_anchor;
let if_block0 = ctx[4] && create_if_block_5(ctx);
let if_block1 = ctx[5].length && create_if_block$1(ctx);
return { return {
c() { c() {
if (if_block) if (if_block0)
if_block.c(); if_block0.c();
if_block_anchor = empty(); t = space();
if (if_block1)
if_block1.c();
if_block1_anchor = empty();
this.c = noop; this.c = noop;
}, },
m(target, anchor) { m(target, anchor) {
if (if_block) if (if_block0)
if_block.m(target, anchor); if_block0.m(target, anchor);
insert(target, if_block_anchor, anchor); insert(target, t, anchor);
if (if_block1)
if_block1.m(target, anchor);
insert(target, if_block1_anchor, anchor);
}, },
p(ctx2, [dirty]) { p(ctx2, [dirty]) {
if (ctx2[4].length) { if (ctx2[4]) {
if (if_block) { if (if_block0) {
if_block.p(ctx2, dirty); if_block0.p(ctx2, dirty);
} else { } else {
if_block = create_if_block$1(ctx2); if_block0 = create_if_block_5(ctx2);
if_block.c(); if_block0.c();
if_block.m(if_block_anchor.parentNode, if_block_anchor); if_block0.m(t.parentNode, t);
} }
} else if (if_block) { } else if (if_block0) {
if_block.d(1); if_block0.d(1);
if_block = null; if_block0 = null;
}
if (ctx2[5].length) {
if (if_block1) {
if_block1.p(ctx2, dirty);
} else {
if_block1 = create_if_block$1(ctx2);
if_block1.c();
if_block1.m(if_block1_anchor.parentNode, if_block1_anchor);
}
} else if (if_block1) {
if_block1.d(1);
if_block1 = null;
} }
}, },
i: noop, i: noop,
o: noop, o: noop,
d(detaching) { d(detaching) {
if (if_block) if (if_block0)
if_block.d(detaching); if_block0.d(detaching);
if (detaching) if (detaching)
detach(if_block_anchor); detach(t);
if (if_block1)
if_block1.d(detaching);
if (detaching)
detach(if_block1_anchor);
} }
}; };
} }
@@ -799,6 +845,7 @@ function instance$1($$self, $$props, $$invalidate) {
let { theme = "light" } = $$props; let { theme = "light" } = $$props;
let { show_recurrent = false } = $$props; let { show_recurrent = false } = $$props;
let { sidebar = "true" } = $$props; let { sidebar = "true" } = $$props;
let { external_style = "" } = $$props;
let mounted = false; let mounted = false;
let events = []; let events = [];
function update2(v) { function update2(v) {
@@ -814,11 +861,9 @@ function instance$1($$self, $$props, $$invalidate) {
if (places) { if (places) {
params.push(`places=${places}`); params.push(`places=${places}`);
} }
if (show_recurrent) { params.push(`show_recurrent=${show_recurrent ? "true" : "false"}`);
params.push(`show_recurrent=true`);
}
fetch(`${baseurl}/api/events?${params.join("&")}`).then((res) => res.json()).then((e) => { fetch(`${baseurl}/api/events?${params.join("&")}`).then((res) => res.json()).then((e) => {
$$invalidate(4, events = e); $$invalidate(5, events = e);
}).catch((e) => { }).catch((e) => {
console.error("Error loading Gancio API -> ", e); console.error("Error loading Gancio API -> ", e);
}); });
@@ -833,20 +878,22 @@ function instance$1($$self, $$props, $$invalidate) {
if ("title" in $$props2) if ("title" in $$props2)
$$invalidate(1, title = $$props2.title); $$invalidate(1, title = $$props2.title);
if ("maxlength" in $$props2) if ("maxlength" in $$props2)
$$invalidate(5, maxlength = $$props2.maxlength); $$invalidate(6, maxlength = $$props2.maxlength);
if ("tags" in $$props2) if ("tags" in $$props2)
$$invalidate(6, tags = $$props2.tags); $$invalidate(7, tags = $$props2.tags);
if ("places" in $$props2) if ("places" in $$props2)
$$invalidate(7, places = $$props2.places); $$invalidate(8, places = $$props2.places);
if ("theme" in $$props2) if ("theme" in $$props2)
$$invalidate(2, theme = $$props2.theme); $$invalidate(2, theme = $$props2.theme);
if ("show_recurrent" in $$props2) if ("show_recurrent" in $$props2)
$$invalidate(8, show_recurrent = $$props2.show_recurrent); $$invalidate(9, show_recurrent = $$props2.show_recurrent);
if ("sidebar" in $$props2) if ("sidebar" in $$props2)
$$invalidate(3, sidebar = $$props2.sidebar); $$invalidate(3, sidebar = $$props2.sidebar);
if ("external_style" in $$props2)
$$invalidate(4, external_style = $$props2.external_style);
}; };
$$self.$$.update = () => { $$self.$$.update = () => {
if ($$self.$$.dirty & 494) { if ($$self.$$.dirty & 974) {
update2(); update2();
} }
}; };
@@ -855,6 +902,7 @@ function instance$1($$self, $$props, $$invalidate) {
title, title,
theme, theme,
sidebar, sidebar,
external_style,
events, events,
maxlength, maxlength,
tags, tags,
@@ -873,12 +921,13 @@ class GancioEvents extends SvelteElement {
}, instance$1, create_fragment$1, safe_not_equal, { }, instance$1, create_fragment$1, safe_not_equal, {
baseurl: 0, baseurl: 0,
title: 1, title: 1,
maxlength: 5, maxlength: 6,
tags: 6, tags: 7,
places: 7, places: 8,
theme: 2, theme: 2,
show_recurrent: 8, show_recurrent: 9,
sidebar: 3 sidebar: 3,
external_style: 4
}, null); }, null);
if (options) { if (options) {
if (options.target) { if (options.target) {
@@ -899,7 +948,8 @@ class GancioEvents extends SvelteElement {
"places", "places",
"theme", "theme",
"show_recurrent", "show_recurrent",
"sidebar" "sidebar",
"external_style"
]; ];
} }
get baseurl() { get baseurl() {
@@ -917,21 +967,21 @@ class GancioEvents extends SvelteElement {
flush(); flush();
} }
get maxlength() { get maxlength() {
return this.$$.ctx[5]; return this.$$.ctx[6];
} }
set maxlength(maxlength) { set maxlength(maxlength) {
this.$$set({ maxlength }); this.$$set({ maxlength });
flush(); flush();
} }
get tags() { get tags() {
return this.$$.ctx[6]; return this.$$.ctx[7];
} }
set tags(tags) { set tags(tags) {
this.$$set({ tags }); this.$$set({ tags });
flush(); flush();
} }
get places() { get places() {
return this.$$.ctx[7]; return this.$$.ctx[8];
} }
set places(places) { set places(places) {
this.$$set({ places }); this.$$set({ places });
@@ -945,7 +995,7 @@ class GancioEvents extends SvelteElement {
flush(); flush();
} }
get show_recurrent() { get show_recurrent() {
return this.$$.ctx[8]; return this.$$.ctx[9];
} }
set show_recurrent(show_recurrent) { set show_recurrent(show_recurrent) {
this.$$set({ show_recurrent }); this.$$set({ show_recurrent });
@@ -958,6 +1008,13 @@ class GancioEvents extends SvelteElement {
this.$$set({ sidebar }); this.$$set({ sidebar });
flush(); flush();
} }
get external_style() {
return this.$$.ctx[4];
}
set external_style(external_style) {
this.$$set({ external_style });
flush();
}
} }
customElements.define("gancio-events", GancioEvents); customElements.define("gancio-events", GancioEvents);
function create_if_block(ctx) { function create_if_block(ctx) {
@@ -996,7 +1053,7 @@ function create_if_block(ctx) {
t6 = text(t6_value); t6 = text(t6_value);
attr(div1, "class", "place"); attr(div1, "class", "place");
attr(div2, "class", "container"); attr(div2, "class", "container");
attr(a, "href", a_href_value = "" + (ctx[0] + "/event/" + (ctx[1].slug || ctx[1].id))); attr(a, "href", a_href_value = ctx[0] + "/event/" + (ctx[1].slug || ctx[1].id));
attr(a, "class", "card"); attr(a, "class", "card");
attr(a, "target", "_blank"); attr(a, "target", "_blank");
}, },
@@ -1035,7 +1092,7 @@ function create_if_block(ctx) {
set_data(t3, t3_value); set_data(t3, t3_value);
if (dirty & 2 && t6_value !== (t6_value = ctx2[1].place.name + "")) if (dirty & 2 && t6_value !== (t6_value = ctx2[1].place.name + ""))
set_data(t6, t6_value); set_data(t6, t6_value);
if (dirty & 3 && a_href_value !== (a_href_value = "" + (ctx2[0] + "/event/" + (ctx2[1].slug || ctx2[1].id)))) { if (dirty & 3 && a_href_value !== (a_href_value = ctx2[0] + "/event/" + (ctx2[1].slug || ctx2[1].id))) {
attr(a, "href", a_href_value); attr(a, "href", a_href_value);
} }
}, },

View File

@@ -8,6 +8,24 @@ nav_order: 10
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
### 1.4.4 - 10 may '22
- better img rendering, make it easier to download flyer #153
- avoid place and tags duplication (remove white space, match case insensitive)
- show date and place to unconfirmed events
- add warning when visiting from different hostname or protocol #149
- add tags and fix html description in ics export
- add git dependency in Dockerfile #148
- add external_style param to gancio-events webcomponent
- add GANCIO_HOST and GANCIO_PORT environment vars
- fix place and address when importing from url #147
- fix user account removal
- fix timezone issue #151
- fix scrolling behavior
- fix adding event on disabled anon posting
- fix plain description meta
- fix recurrent events always shown #150
- remove `less` and `less-loader` dependency
### 1.4.3 - 10 mar '22 ### 1.4.3 - 10 mar '22
- fix [#140](https://framagit.org/les/gancio/-/issues/140) - Invalid date - fix [#140](https://framagit.org/les/gancio/-/issues/140) - Invalid date
- fix [#141](https://framagit.org/les/gancio/-/issues/141) - Cannot change logo - fix [#141](https://framagit.org/les/gancio/-/issues/141) - Cannot change logo

View File

@@ -1,4 +1,5 @@
FROM node:17.4-slim FROM node:17-slim
RUN bash -c "apt update -y && apt install git -y && apt clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp"
RUN yarn global remove gancio || true RUN yarn global remove gancio || true
RUN yarn cache clean RUN yarn cache clean
RUN yarn global add --latest --production --silent https://gancio.org/latest.tgz RUN yarn global add --latest --production --silent https://gancio.org/latest.tgz

View File

@@ -4,7 +4,7 @@ services:
gancio: gancio:
build: . build: .
restart: always restart: always
image: node:17.4-slim image: gancio
container_name: gancio container_name: gancio
environment: environment:
- PATH=$PATH:/home/node/.yarn/bin - PATH=$PATH:/home/node/.yarn/bin
@@ -13,7 +13,7 @@ services:
- GANCIO_DB_DIALECT=sqlite - GANCIO_DB_DIALECT=sqlite
- GANCIO_DB_STORAGE=./gancio.sqlite - GANCIO_DB_STORAGE=./gancio.sqlite
entrypoint: /entrypoint.sh entrypoint: /entrypoint.sh
command: gancio start --docker command: gancio start
volumes: volumes:
- ./data:/home/node/data - ./data:/home/node/data
ports: ports:

View File

@@ -16,7 +16,7 @@ services:
gancio: gancio:
build: . build: .
restart: always restart: always
image: node:17.4-slim image: gancio
container_name: gancio container_name: gancio
environment: environment:
- PATH=$PATH:/home/node/.yarn/bin - PATH=$PATH:/home/node/.yarn/bin
@@ -24,10 +24,11 @@ services:
- NODE_ENV=production - NODE_ENV=production
- GANCIO_DB_DIALECT=mariadb - GANCIO_DB_DIALECT=mariadb
- GANCIO_DB_HOST=db - GANCIO_DB_HOST=db
- GANCIO_DB_PORT=3306
- GANCIO_DB_DATABASE=gancio - GANCIO_DB_DATABASE=gancio
- GANCIO_DB_USERNAME=gancio - GANCIO_DB_USERNAME=gancio
- GANCIO_DB_PASSWORD=gancio - GANCIO_DB_PASSWORD=gancio
command: gancio start --docker command: gancio start
entrypoint: /entrypoint.sh entrypoint: /entrypoint.sh
volumes: volumes:
- ./data:/home/node/data - ./data:/home/node/data

View File

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

View File

@@ -4,7 +4,7 @@ services:
gancio: gancio:
build: . build: .
restart: always restart: always
image: node:17.4-slim image: gancio
container_name: gancio container_name: gancio
environment: environment:
- PATH=$PATH:/home/node/.yarn/bin - PATH=$PATH:/home/node/.yarn/bin
@@ -13,7 +13,7 @@ services:
- GANCIO_DB_DIALECT=sqlite - GANCIO_DB_DIALECT=sqlite
- GANCIO_DB_STORAGE=./gancio.sqlite - GANCIO_DB_STORAGE=./gancio.sqlite
entrypoint: /entrypoint.sh entrypoint: /entrypoint.sh
command: gancio start --docker command: gancio start
volumes: volumes:
- ./data:/home/node/data - ./data:/home/node/data
ports: ports:

View File

@@ -24,14 +24,14 @@ nowhere on gancio does the identity of who posted an event appear, not even unde
- **Anonymous events**: optionally a visitor can create events without being registered (an administrator must confirm them) - **Anonymous events**: optionally a visitor can create events without being registered (an administrator must confirm them)
- **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) - **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 usage/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 - Very easy UI
- Multi-day events (festival, conferences...) - Multi-day events (festival, conferences...)
- Recurring events (each monday, each two monday, each monday and friday, each two saturday, etc.) - Recurring events (each monday, each two monday, each monday and friday, each two saturday, etc.)
- Filter events for tags or places - Filter events for tags or places
- RSS and ICS export (with filters) - RSS and ICS export (with filters)
- embed your events in your website with [webcomponents]({% link embed.md %}) or iframe ([example](https://gancio.cisti.org/embed/list?title=Upcoming events)) - embed your events in your website with [webcomponents]({% link usage/embed.md %}) or iframe ([example](https://gancio.cisti.org/embed/list?title=Upcoming events))
- boost / bookmark / comment events from the fediverse! - boost / bookmark / comment events from the fediverse!
- Lot of configurations available (dark/light theme, user registration open/close, enable federation, enable recurring events) - Lot of configurations available (dark/light theme, user registration open/close, enable federation, enable recurring events)

View File

@@ -65,6 +65,7 @@ You'll need to [setup nginx as a proxy]({% link install/nginx.md %}) then you ca
```bash ```bash
cd /opt/gancio cd /opt/gancio # or where your installation is
wget https://gancio.org/docker/Dockerfile -O Dockerfile
docker-compose up -d --no-deps --build docker-compose up -d --no-deps --build
``` ```

View File

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

View File

@@ -9,3 +9,5 @@ has_children: true
## Usage ## Usage
ehmmm, help needed here :smile: feel free to send a PR => [here](https://framagit.org/les/gancio/tree/master/docs) ehmmm, help needed here :smile: feel free to send a PR => [here](https://framagit.org/les/gancio/tree/master/docs)

View File

@@ -1,14 +1,21 @@
<template lang='pug'> <template>
v-app(app) <v-app app>
Snackbar <Snackbar/>
Confirm <Confirm/>
Nav <Nav/>
<v-main app>
<div class="ml-1 mb-1 mt-1" v-if='showCohorts || showBack'>
<v-btn v-show='showBack' text color='primary' to='/'><v-icon v-text='mdiChevronLeft'/></v-btn>
<v-btn v-for='cohort in cohorts' text color='primary' :key='cohort.id' :to='`/g/${cohort.name}`'>{{cohort.name}}</v-btn>
</div>
<v-fade-transition hide-on-leave>
<nuxt />
</v-fade-transition>
</v-main>
<Footer/>
v-main(app) </v-app>
v-fade-transition(hide-on-leave)
nuxt
Footer
</template> </template>
<script> <script>
@@ -17,6 +24,7 @@ import Snackbar from '../components/Snackbar'
import Footer from '../components/Footer' import Footer from '../components/Footer'
import Confirm from '../components/Confirm' import Confirm from '../components/Confirm'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import { mdiChevronLeft } from '@mdi/js'
export default { export default {
head () { head () {
@@ -26,9 +34,24 @@ export default {
} }
} }
}, },
data () {
return { cohorts: [], mdiChevronLeft }
},
async fetch () {
this.cohorts = await this.$axios.$get('cohorts')
},
name: 'Default', name: 'Default',
components: { Nav, Snackbar, Footer, Confirm }, components: { Nav, Snackbar, Footer, Confirm },
computed: mapState(['settings', 'locale']), computed: {
...mapState(['settings', 'locale']),
showBack () {
return ['tag-tag', 'g-cohort', 'p-place', 'search', 'announcement-id'].includes(this.$route.name)
},
showCohorts () {
if (!this.cohorts || this.cohorts.length === 0) return false
return ['tag-tag', 'index', 'g-cohort', 'p-place'].includes(this.$route.name)
}
},
created () { created () {
this.$vuetify.theme.dark = this.settings['theme.is_dark'] this.$vuetify.theme.dark = this.settings['theme.is_dark']
} }

View File

@@ -2,7 +2,7 @@
v-app#iframe v-app#iframe
nuxt nuxt
</template> </template>
<style lang='less'> <style>
#iframe.v-application { #iframe.v-application {
background-color: transparent !important; background-color: transparent !important;
} }

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" "list_description": "Si tens una web i vols encastar una llista d'activitats, pots fer servir el codi de sota"
}, },
"register": { "register": {
"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.", "description": "Els moviments socials necessitem organitzar-nos i auto-finançar-nos.<br/>\n<br/>Abans que puguis publicar, <strong> hem d'aprovar el teu compte </strong>, tingues en compte 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: ", "error": "Error: ",
"complete": "El registre ha de ser confirmat.", "complete": "El registre ha de ser confirmat.",
"first_user": "S'ha creat i activat un compte administrador" "first_user": "S'ha creat i activat un compte administrador"
@@ -124,7 +124,7 @@
"media_description": "Pots adjuntar un cartell (opcional)", "media_description": "Pots adjuntar un cartell (opcional)",
"added": "S'ha afegit l'activitat", "added": "S'ha afegit l'activitat",
"added_anon": "S'ha afegit l'activitat però encara ha de ser confirmada.", "added_anon": "S'ha afegit l'activitat però encara ha de ser confirmada.",
"where_description": "On es farà? Si no està posat, escriu-ho i <b>prem Enter</b>.", "where_description": "On es farà? Si no està posat, escriu-ho i prem Enter.",
"confirmed": "S'ha confirmat l'activitat", "confirmed": "S'ha confirmat l'activitat",
"not_found": "No s'ha trobat l'activitat", "not_found": "No s'ha trobat l'activitat",
"remove_confirmation": "Segur que vols esborrar l'activitat?", "remove_confirmation": "Segur que vols esborrar l'activitat?",
@@ -150,7 +150,7 @@
"from": "Des de les", "from": "Des de les",
"image_too_big": "La imatge és massa gran! Max 4 MB", "image_too_big": "La imatge és massa gran! Max 4 MB",
"interact_with_me_at": "Interacciona amb mi a", "interact_with_me_at": "Interacciona amb mi a",
"follow_me_description": "Entre les diverses maneres d'estar al dia de les activitats que es publiquen aquí a {title},\n pots seguir-nos al compte <u>{account}</u> des de Mastodon o altres, i afegir recursos des d'allà. <br/> <br/>\nSi no has sentit mai sobre «Mastodon» o «Fedivers», recomanem mirar <a href='https://peertube.social/videos/watch/d9bd2ee9-b7a4-44e3-8d65-61badd15c6e6'> aquest vídeo (subtitulat en català)</a>. <br/> <br/> Introdueix la teva instància a sota (ex: red.confederac.io o mastodont.cat)", "follow_me_description": "Entre les diverses maneres d'estar al dia de les activitats que es publiquen aquí a {title},\n pots seguir-nos al compte <u>{account}</u> des de Mastodon o altres, i afegir recursos des d'allà. <br/> <br/>\nSi no has sentit mai sobre «Mastodon» o «Fedivers», recomanem fer un cop d'ull a <a href='https://equipamentslliures.cat/divulgacio/fediverse'>aquesta breu introducció al Fedivers</a>. <br/> <br/> Introdueix la teva instància a sota (ex: kolektiva.social o mastodont.cat)",
"interact_with_me": "Segueix-nos al fedivers", "interact_with_me": "Segueix-nos al fedivers",
"remove_recurrent_confirmation": "Estàs segur/a d'esborrar aquesta activitat periòdica?\nNo s'esborraran les ocurrències antigues, només es deixaran de crear les futures.", "remove_recurrent_confirmation": "Estàs segur/a d'esborrar aquesta activitat periòdica?\nNo s'esborraran les ocurrències antigues, només es deixaran de crear les futures.",
"ics": "ICS", "ics": "ICS",
@@ -159,7 +159,11 @@
"edit_recurrent": "Edita l'activitat periòdica:", "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", "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)" "import_description": "Pots importar activitats des d'altres instàncies o plataformes que facin servir formats estàndards (ics o h-event)",
"remove_media_confirmation": "Confirmeu l'eliminació de la imatge?",
"download_flyer": "Baixa el flyer",
"alt_text_description": "Descripció per a persones amb discapacitat visual",
"choose_focal_point": "Tria el punt focal"
}, },
"admin": { "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.", "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.",
@@ -226,7 +230,9 @@
"smtp_hostname": "Amfitrió SMTP (hostname)", "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_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_button": "Envia un correu de prova", "smtp_test_button": "Envia un correu de prova",
"widget": "Giny" "widget": "Giny",
"wrong_domain_warning": "La url base configurada a config.json <b>({baseurl})</b> difereix de la que esteu visitant <b>({url})</b>",
"event_remove_ok": "S'ha suprimit l'esdeveniment"
}, },
"auth": { "auth": {
"not_confirmed": "Encara no s'ha confirmat…", "not_confirmed": "Encara no s'ha confirmat…",
@@ -272,6 +278,8 @@
"setup": { "setup": {
"completed": "S'ha completat la configuració inicial", "completed": "S'ha completat la configuració inicial",
"completed_description": "<p>Ara ja pots entrar amb aquesta usuària:<br/><br/>Nom: <b>{email}</b><br/>Contrasenya: <b>{password}<b/></p>", "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" "start": "Comença",
"copy_password_dialog": "Sí, has de copiar la contrasenya!",
"https_warning": "Esteu visitant des d'HTTP, recordeu canviar baseurl a config.json si canvieu a HTTPS!"
} }
} }

View File

@@ -24,5 +24,9 @@
}, },
"event_confirm": { "event_confirm": {
"content": "Puede confirmar este evento <a href='{{url}}'>aquí</a>" "content": "Puede confirmar este evento <a href='{{url}}'>aquí</a>"
},
"test": {
"subject": "Tu configuración SMTP funciona",
"content": "Esto es un email de prueba. Si estás leyendo esto es que tu configuración funciona."
} }
} }

View File

@@ -86,7 +86,8 @@
"reset": "Reset", "reset": "Reset",
"import": "Import", "import": "Import",
"max_events": "N. max events", "max_events": "N. max events",
"label": "Label" "label": "Label",
"blobs": "Blobs"
}, },
"login": { "login": {
"description": "By logging in you can publish new events.", "description": "By logging in you can publish new events.",
@@ -159,7 +160,11 @@
"import_URL": "Import from URL", "import_URL": "Import from URL",
"import_ICS": "Import from ICS", "import_ICS": "Import from ICS",
"ics": "ICS", "ics": "ICS",
"import_description": "You can import events from other platforms and other instances through standard formats (ics and h-event)" "import_description": "You can import events from other platforms and other instances through standard formats (ics and h-event)",
"alt_text_description": "Description for people with visual impairments",
"choose_focal_point": "Choose the focal point",
"remove_media_confirmation": "Do you confirm the image removal?",
"download_flyer": "Download flyer"
}, },
"admin": { "admin": {
"place_description": "If you have gotten the place or address wrong, you can change it.<br/>All current and past events associated with this place will change address.", "place_description": "If you have gotten the place or address wrong, you can change it.<br/>All current and past events associated with this place will change address.",
@@ -170,6 +175,7 @@
"delete_user_confirm": "Are you sure you want to remove {user}?", "delete_user_confirm": "Are you sure you want to remove {user}?",
"user_remove_ok": "User removed", "user_remove_ok": "User removed",
"user_create_ok": "User created", "user_create_ok": "User created",
"event_remove_ok": "Event removed",
"allow_registration_description": "Allow open registrations?", "allow_registration_description": "Allow open registrations?",
"allow_anon_event": "Allow anonymous events (has to be confirmed)?", "allow_anon_event": "Allow anonymous events (has to be confirmed)?",
"allow_recurrent_event": "Allow recurring events", "allow_recurrent_event": "Allow recurring events",
@@ -226,7 +232,8 @@
"smtp_test_success": "A test email is sent to {admin_email}, please check your inbox", "smtp_test_success": "A test email is sent to {admin_email}, please check your inbox",
"smtp_test_button": "Send a test email", "smtp_test_button": "Send a test email",
"admin_email": "Admin e-mail", "admin_email": "Admin e-mail",
"widget": "Widget" "widget": "Widget",
"wrong_domain_warning": "The baseurl configured in config.json <b>({baseurl})</b> differs from the one you're visiting <b>({url})</b>"
}, },
"auth": { "auth": {
"not_confirmed": "Not confirmed yet…", "not_confirmed": "Not confirmed yet…",
@@ -273,6 +280,7 @@
"completed": "Setup completed", "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>", "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!", "copy_password_dialog": "Yes, you have to copy the password!",
"start": "Start" "start": "Start",
"https_warning": "You're visiting from HTTP, remember to change baseurl in config.json if you switch to HTTPS!"
} }
} }

View File

@@ -110,14 +110,14 @@
"list_description": "Si tienes un sitio web y quieres mostrar una lista de eventos, puedes usar el siguiente código" "list_description": "Si tienes un sitio web y quieres mostrar una lista de eventos, puedes usar el siguiente código"
}, },
"register": { "register": {
"description": "Los movimientos sociales necesitan organizarse y autofinanciarse. <br/> Este es un regalo para ustedes, úsenlo solamente para eventos con fines no comerciales y obviamente antifascistas, antisexistas y antirracistas.\n<br/> Antes de que puedas publicar <strong> debemos aprobar la cuenta </strong>. Como imaginarás, <strong> detrás de este sitio hay personas </strong> de carne y hueso, por esto te pedimos escribir algo para hacernos saber que tipos de eventos te gustaría publicar.", "description": "Los movimientos sociales necesitan organizarse y autofinanciarse. <br/>\nEste es un regalo para ustedes, úsenlo solamente para eventos con fines no comerciales y obviamente antifascistas, antisexistas y antirracistas.\n<br/> Antes de que puedas publicar, <strong> debemos aprobar la cuenta </strong>. Como imaginarás, <strong> detrás de este sitio hay personas de carne y hueso</strong>, por esto te pedimos escribir algo para hacernos saber que tipos de eventos te gustaría publicar.",
"error": "Error: ", "error": "Error: ",
"complete": "Confirmaremos el registro lo antes posible.", "complete": "Confirmaremos el registro lo antes posible.",
"first_user": "Administrador creado y activado" "first_user": "Administrador creado y activado"
}, },
"event": { "event": {
"anon": "Anónimo", "anon": "Anónimo",
"anon_description": "Puedes ingresar un evento sin registrarte o iniciar sesión, pero en este caso tendrás que esperar a que alguien lo lea para confirmar que es un evento adecuado para este espacio,\ndelegando esta elección. Además, no será posible modificarlo. <br/> <br/>\nSi no te gusta, puedes <a href='/login'> iniciar sesión </a> o <a href='/register'> registrarte </a>,\nde lo contrario, continúa y recibirás una respuesta lo antes posible. ", "anon_description": "Puedes ingresar un evento sin registrarte o iniciar sesión, pero en este caso tendrás que esperar a que alguien lo lea para confirmar que es un evento adecuado para este espacio,\ndelegando esta elección. Además, no será posible modificarlo. <br/> <br/>\nSi no te gusta, puedes <a href='/login'> iniciar sesión </a> o <a href='/register'> registrarte </a>. De lo contrario, continúa y recibirás una respuesta lo antes posible. ",
"same_day": "Mismo día", "same_day": "Mismo día",
"what_description": "Nombre evento", "what_description": "Nombre evento",
"description_description": "Descripción, puedes copiar y pegar", "description_description": "Descripción, puedes copiar y pegar",
@@ -148,7 +148,7 @@
"from": "Desde las", "from": "Desde las",
"image_too_big": "La imagén es demasiado grande! Tamaño máx 4M", "image_too_big": "La imagén es demasiado grande! Tamaño máx 4M",
"interact_with_me_at": "Sígueme en el fediverso en", "interact_with_me_at": "Sígueme en el fediverso en",
"show_recurrent": "Eventos recurrientes", "show_recurrent": "Eventos recurrentes",
"show_past": "eventos pasados", "show_past": "eventos pasados",
"follow_me_description": "Entre las diversas formas de mantenerse al día con los eventos publicados aquí en {title},\npuedes seguir la cuenta <u>{account}</u> desde el fediverso, por ejemplo, a través de un Mastodon, y posiblemente añadir recursos a un evento desde allí.<br/><br/>\nSi nunca has oído hablar del Mastodon y el fediverso te sugerimos que leas <a href='https://cagizero.wordpress.com/2018/10/25/cose-mastodon/'>este artículo</a>.<br/><br/> Introduce tu instancia abajo (por ejemplo mastodon.cisti.org o mastodon.bida.im)", "follow_me_description": "Entre las diversas formas de mantenerse al día con los eventos publicados aquí en {title},\npuedes seguir la cuenta <u>{account}</u> desde el fediverso, por ejemplo, a través de un Mastodon, y posiblemente añadir recursos a un evento desde allí.<br/><br/>\nSi nunca has oído hablar del Mastodon y el fediverso te sugerimos que leas <a href='https://cagizero.wordpress.com/2018/10/25/cose-mastodon/'>este artículo</a>.<br/><br/> Introduce tu instancia abajo (por ejemplo mastodon.cisti.org o mastodon.bida.im)",
"interact_with_me": "Sigueme en el fediverso", "interact_with_me": "Sigueme en el fediverso",
@@ -221,7 +221,14 @@
"is_dark": "Tema oscuro", "is_dark": "Tema oscuro",
"instance_block_confirm": "¿Estás seguro/a que quieres bloquear la instancia {instance}?", "instance_block_confirm": "¿Estás seguro/a que quieres bloquear la instancia {instance}?",
"add_instance": "Añadir instancia", "add_instance": "Añadir instancia",
"disable_user_confirm": "Estas seguro de que quieres deshabilitar a {user}?" "disable_user_confirm": "Estas seguro de que quieres deshabilitar a {user}?",
"widget": "Widget",
"show_smtp_setup": "Ajustes de correo",
"smtp_hostname": "Nombre del equipo SMTP",
"smtp_test_success": "Un correo de prueba se ha enviado a {admin_email}. Por favos, comprueba tu bandeja de entrada",
"smtp_test_button": "Enviar correo de prueba",
"admin_email": "Correo del administrador",
"smtp_description": "<ul><li>El administrador debería recibir un correo cuando son añadidos eventos anónimos (si está habilitado).</li><li>El administrador debería recibir un correo de petición de registro (si está habilitado).</li><li>El usuario debería recibir un correo de petición de registro.</li><li>El usuario debería recibir un correo de confirmación de registro.</li><li>El usuario debería recibir un correo de confirmación cuando el administrador le subscriba directamente.</li><li>El usuario debería recibir un correo para restaurar la contraseña cuando la haya olvidado.</li></ul>"
}, },
"auth": { "auth": {
"not_confirmed": "Todavía no hemos confirmado este email…", "not_confirmed": "Todavía no hemos confirmado este email…",
@@ -263,5 +270,10 @@
"validators": { "validators": {
"email": "Introduce un correo electrónico valido", "email": "Introduce un correo electrónico valido",
"required": "{fieldName} es requerido" "required": "{fieldName} es requerido"
},
"setup": {
"completed": "Configuración completada",
"start": "Inicio",
"completed_description": "<p>Puedes ingresar con el siguiente usuario:<br/><br/>Usuario: <b>{email}</b><br/>Contraseña: <b>{password}<b/></p>"
} }
} }

View File

@@ -109,7 +109,7 @@
"list_description": "Webgune bat baduzu eta ekitaldien zerrenda erakutsi nahi baduzu, ondorengo kodea erabili" "list_description": "Webgune bat baduzu eta ekitaldien zerrenda erakutsi nahi baduzu, ondorengo kodea erabili"
}, },
"register": { "register": {
"description": "Herri mugimenduek autoantolaketaren bidean diru-iturrien beharrak dauzkatela badakigu.<br/>Honako hauxe oparitxoa da, hortaz erabili ezazue ekitaldi ez-komertzialak iragartzeko, eta esan gabe doa, ekitaldi antifaxistak, antisexistak eta antiarriztetarako :) .\n<br/>Argitaratzen hasi baino lehen<strong> zure kontu berriak onarpena jaso beharko du </strong>beraz, <strong>webgune honen atzean hezur-haragizko pertsonak gaudela jakinda </strong>, (momenutz euskal 'AI'-rik ez daukagu baina adi, agertuko direla) idatzi iezaguzu lerro batzuk argitaratu nahi dituzun ekitaldiei buruz.", "description": "Gizarte mugimenduak beraien kabuz antolatu behar dira.<br/>\n<br/>Argitaratzen hasi baino lehen <strong>zure kontu berria onartua izan behar da</strong>, beraz, <strong>webgune honen atzean hezur-haragizko pertsonak</strong> gaudela kontuan izanik, azal iezaguzu mesedez pare bat lerrotan zer nolako ekitaldiak argitaratu nahi dituzun.",
"error": "Errorea: ", "error": "Errorea: ",
"complete": "Izen-ematea baieztatua izan behar da.", "complete": "Izen-ematea baieztatua izan behar da.",
"first_user": "Administratzailea sortu da" "first_user": "Administratzailea sortu da"
@@ -159,7 +159,11 @@
"only_future": "datozen ekitaldiak bakarrik", "only_future": "datozen ekitaldiak bakarrik",
"edit_recurrent": "Editatu ekitaldi errepikaria:", "edit_recurrent": "Editatu ekitaldi errepikaria:",
"updated": "Ekitaldia eguneratu da", "updated": "Ekitaldia eguneratu da",
"saved": "Ekitaldia gorde da" "saved": "Ekitaldia gorde da",
"remove_media_confirmation": "Irudiaren ezabaketa baieztatzen duzu?",
"alt_text_description": "Ikusmen-urritasunak dituztenentzako deskripzioa",
"choose_focal_point": "Aukeratu arretagunea",
"download_flyer": "Deskargatu eskuorria"
}, },
"admin": { "admin": {
"place_description": "Helbidea oker badago, alda dezakezu.<br/>Leku honekin lotutako iraganeko eta etorkizuneko ekitaldien helbidea aldatuko da.", "place_description": "Helbidea oker badago, alda dezakezu.<br/>Leku honekin lotutako iraganeko eta etorkizuneko ekitaldien helbidea aldatuko da.",
@@ -225,7 +229,10 @@
"smtp_test_success": "Probako eposta bidali da {admin_email}-(e)ra, begiratu zure sarrera-ontzia", "smtp_test_success": "Probako eposta bidali da {admin_email}-(e)ra, begiratu zure sarrera-ontzia",
"admin_email": "Administratzailearen eposta", "admin_email": "Administratzailearen eposta",
"smtp_hostname": "SMTP hostname", "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>" "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>",
"widget": "Tresna",
"event_remove_ok": "Ekitaldia ezabatu da",
"wrong_domain_warning": "config.json-en konfiguratuta dagoen baseurl <b>({baseurl})</b> ez da bisitatzen ari zaren berbera <b>({url})</b>"
}, },
"auth": { "auth": {
"not_confirmed": "Oraindik baieztatu gabe dago…", "not_confirmed": "Oraindik baieztatu gabe dago…",
@@ -271,6 +278,8 @@
"setup": { "setup": {
"start": "Hasi", "start": "Hasi",
"completed": "Instalazioa bukatu da", "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>" "completed_description": "<p>Erabiltzaile honekin saioa has dezakezu orain:<br/><br/>Erabiltzailea: <b>{email}</b><br/>Pasahitza: <b>{password}<b/></p>",
"copy_password_dialog": "Bai, pasahitza kopiatu behar duzu!",
"https_warning": "HTTP bidez ari zarela kontuan izan. HTTPSra pasatzen bazara gogoratu config.json-en baseurl aldatzeaz!"
} }
} }

View File

@@ -133,7 +133,10 @@
"edit_recurrent": "Modifier lévènement récurrent :", "edit_recurrent": "Modifier lévènement récurrent :",
"updated": "Évènement mis à jour", "updated": "Évènement mis à jour",
"import_description": "Vous pouvez importer des événements depuis d'autres plateformes ou d'autres instances à travers des formats standards (ics et h-event)", "import_description": "Vous pouvez importer des événements depuis d'autres plateformes ou d'autres instances à travers des formats standards (ics et h-event)",
"saved": "Événement enregistré" "saved": "Événement enregistré",
"alt_text_description": "Description pour les personnes avec une déficience visuelle",
"remove_media_confirmation": "Confirmer la suppression de l'image ?",
"download_flyer": "Télécharger le flyer"
}, },
"register": { "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</strong>, à 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.",
@@ -273,6 +276,7 @@
"check_db": "Vérifier la base de données", "check_db": "Vérifier la base de données",
"completed": "Configuration terminée", "completed": "Configuration terminée",
"completed_description": "<p>Vous pouvez désormais vous connectez avec le compte utilisateur suivant :<br/><br/>Identifiant : <b>{email}</b><br/>Mot de passe : <b>{password}<b/></p>", "completed_description": "<p>Vous pouvez désormais vous connectez avec le compte utilisateur suivant :<br/><br/>Identifiant : <b>{email}</b><br/>Mot de passe : <b>{password}<b/></p>",
"start": "Commencer" "start": "Commencer",
"copy_password_dialog": "Oui, vous devez copier le mot de passe !"
} }
} }

View File

@@ -150,7 +150,11 @@
"each_2w": "Cada dúas semanas", "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)", "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", "ics": "ICS",
"import_description": "Podes importar eventos desde outras plataformas e outras instancias usando formatos estándar (ics e h-event)" "import_description": "Podes importar eventos desde outras plataformas e outras instancias usando formatos estándar (ics e h-event)",
"alt_text_description": "Descrición para persoas con problemas de visión",
"choose_focal_point": "Elixe onde centrar a atención",
"remove_media_confirmation": "Confirmas a eliminación da imaxe?",
"download_flyer": "Descargar folleto"
}, },
"admin": { "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.", "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.",
@@ -217,7 +221,9 @@
"show_smtp_setup": "Axustes do email", "show_smtp_setup": "Axustes do email",
"smtp_hostname": "Servidor SMTP", "smtp_hostname": "Servidor SMTP",
"new_announcement": "Novo anuncio", "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>" "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>",
"wrong_domain_warning": "O url base configurado en config.json <b>({baseurl)</b> é diferente ao que estás a visitar <b>({url})</b>",
"event_remove_ok": "Evento eliminado"
}, },
"auth": { "auth": {
"not_confirmed": "Aínda non foi confirmado…", "not_confirmed": "Aínda non foi confirmado…",
@@ -263,7 +269,9 @@
"setup": { "setup": {
"completed": "Configuración completada", "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>", "completed_description": "<p>Xa podes acceder con estas credenciais:<br/><br/>Identificador: <b>{email}</b><br/>Contrasinal: <b>{password}</b></p>",
"start": "Comezar" "start": "Comezar",
"https_warning": "Estás entrando con HTTP, lembra cambiar baseurl no config.json se cambias a HTTPS!",
"copy_password_dialog": "Si, tes que copiar o contrasinal!"
}, },
"login": { "login": {
"forgot_password": "Esqueceches o contrasinal?", "forgot_password": "Esqueceches o contrasinal?",

View File

@@ -86,7 +86,8 @@
"reset": "Reset", "reset": "Reset",
"import": "Importa", "import": "Importa",
"max_events": "N. massimo eventi", "max_events": "N. massimo eventi",
"label": "Etichetta" "label": "Etichetta",
"blobs": "Bolle"
}, },
"login": { "login": {
"description": "Entrando puoi pubblicare nuovi eventi.", "description": "Entrando puoi pubblicare nuovi eventi.",
@@ -151,7 +152,7 @@
"each_month": "Ogni mese", "each_month": "Ogni mese",
"due": "alle", "due": "alle",
"from": "Dalle", "from": "Dalle",
"image_too_big": "L'immagine non può essere più grande di 4 MB", "image_too_big": "L'immagine non può essere più grande di 4MB",
"interact_with_me": "Seguimi dal fediverso", "interact_with_me": "Seguimi dal fediverso",
"follow_me_description": "Tra i vari modi di rimanere aggiornati degli eventi pubblicati qui su {title},\npuoi seguire l'account <u>{account}</u> dal fediverso, ad esempio via Mastodon, ed eventualmente aggiungere risorse ad un evento da lì.<br/><br/>\nSe non hai mai sentito parlare di Mastodon e del fediverso ti consigliamo di leggere <a href='https://cagizero.wordpress.com/2018/10/25/cose-mastodon/'>questo articolo</a>.<br/><br/> Inserisci la tua istanza qui sotto (es. mastodon.cisti.org o mastodon.bida.im)", "follow_me_description": "Tra i vari modi di rimanere aggiornati degli eventi pubblicati qui su {title},\npuoi seguire l'account <u>{account}</u> dal fediverso, ad esempio via Mastodon, ed eventualmente aggiungere risorse ad un evento da lì.<br/><br/>\nSe non hai mai sentito parlare di Mastodon e del fediverso ti consigliamo di leggere <a href='https://cagizero.wordpress.com/2018/10/25/cose-mastodon/'>questo articolo</a>.<br/><br/> Inserisci la tua istanza qui sotto (es. mastodon.cisti.org o mastodon.bida.im)",
"only_future": "solo eventi futuri", "only_future": "solo eventi futuri",
@@ -160,9 +161,10 @@
"import_URL": "Importa da URL (ics o h-event)", "import_URL": "Importa da URL (ics o h-event)",
"ics": "ICS", "ics": "ICS",
"import_description": "Puoi importare eventi da altre piattaforme e da altre istanze attraverso i formati standard (ics e h-event)", "import_description": "Puoi importare eventi da altre piattaforme e da altre istanze attraverso i formati standard (ics e h-event)",
"alt_text_description": "Descrizione per utenti con disabilità visive", "alt_text_description": "Descrizione per persone con disabilità visive",
"choose_focal_point": "Scegli il punto centrale cliccando", "choose_focal_point": "Scegli il punto centrale cliccando",
"remove_media_confirmation": "Confermi l'eliminazione dell'immagine?" "remove_media_confirmation": "Confermi l'eliminazione dell'immagine?",
"download_flyer": "Scarica volantino"
}, },
"admin": { "admin": {
"place_description": "Nel caso in cui un luogo sia errato o cambi indirizzo, puoi modificarlo.<br/>Considera che tutti gli eventi associati a questo luogo cambieranno indirizzo (anche quelli passati).", "place_description": "Nel caso in cui un luogo sia errato o cambi indirizzo, puoi modificarlo.<br/>Considera che tutti gli eventi associati a questo luogo cambieranno indirizzo (anche quelli passati).",
@@ -276,6 +278,7 @@
"completed": "Setup completato", "completed": "Setup completato",
"completed_description": "<p>Puoi entrare con le seguenti credenziali:<br/><br/>Utente: <b>{email}</b><br/>Password: <b>{password}<b/></p>", "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!", "copy_password_dialog": "Sì, devi copiare la password!",
"start": "Inizia" "start": "Inizia",
"https_warning": "Stai visitando il setup da HTTP, ricorda di cambiare il baseurl nel config.json quando passerai ad HTTPS!"
} }
} }

View File

@@ -14,7 +14,6 @@ module.exports = {
{ name: 'viewport', content: 'width=device-width, initial-scale=1' } { name: 'viewport', content: 'width=device-width, initial-scale=1' }
], ],
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 }], script: [{ src: '/gancio-events.es.js', async: true, body: true }],
}, },
dev: isDev, dev: isDev,
@@ -27,13 +26,12 @@ module.exports = {
} }
}, },
css: ['./assets/style.less'], css: ['./assets/style.css'],
/* /*
** Customize the progress-bar component ** Customize the progress-bar component
*/ */
loading: '~/components/Loading.vue', loading: '~/components/Loading.vue',
/* /*
** Plugins to load before mounting the App ** Plugins to load before mounting the App
*/ */
@@ -53,9 +51,27 @@ module.exports = {
// Doc: https://axios.nuxtjs.org/usage // Doc: https://axios.nuxtjs.org/usage
'@nuxtjs/axios', '@nuxtjs/axios',
'@nuxtjs/auth', '@nuxtjs/auth',
'@/server/initialize.server.js' '@nuxtjs/sitemap'
], ],
sitemap: {
hostname: config.baseurl,
gzip: true,
exclude: [
'/Admin',
'/settings',
'/export',
'/setup'
],
routes: async () => {
if (config.status === 'READY') {
const Event = require('./server/api/models/event')
const events = await Event.findAll({where: { is_visible: true }})
return events.map(e => `/event/${e.slug}`)
}
}
},
serverMiddleware: ['server/routes'], serverMiddleware: ['server/routes'],
/* /*
@@ -93,7 +109,6 @@ module.exports = {
}, },
buildModules: ['@nuxtjs/vuetify'], buildModules: ['@nuxtjs/vuetify'],
vuetify: { vuetify: {
customVariables: ['~/assets/variables.scss'],
treeShake: true, treeShake: true,
theme: { theme: {
options: { options: {

View File

@@ -1,6 +1,6 @@
{ {
"name": "gancio", "name": "gancio",
"version": "1.4.3", "version": "1.5.0-rc.2",
"description": "A shared agenda for local communities", "description": "A shared agenda for local communities",
"author": "lesion", "author": "lesion",
"scripts": { "scripts": {
@@ -12,7 +12,8 @@
"doc": "cd docs && bundle exec jekyll b", "doc": "cd docs && bundle exec jekyll b",
"doc:dev": "cd docs && bundle exec jekyll s --drafts", "doc:dev": "cd docs && bundle exec jekyll s --drafts",
"migrate": "NODE_ENV=production sequelize db:migrate", "migrate": "NODE_ENV=production sequelize db:migrate",
"migrate:dev": "sequelize db:migrate" "migrate:dev": "sequelize db:migrate",
"build:wc": "cd webcomponents; yarn build:lib; cp dist/gancio-events.es.js ../wp-plugin/js/; cp dist/gancio-events.es.js ../assets/; cp dist/gancio-events.es.js ../docs/assets/js/"
}, },
"files": [ "files": [
"server/", "server/",
@@ -27,19 +28,20 @@
"yarn.lock" "yarn.lock"
], ],
"dependencies": { "dependencies": {
"@mdi/js": "^6.5.95", "@mdi/js": "^6.7.96",
"@nuxtjs/auth": "^4.9.1", "@nuxtjs/auth": "^4.9.1",
"@nuxtjs/axios": "^5.13.5", "@nuxtjs/axios": "^5.13.5",
"@nuxtjs/sitemap": "^2.4.0",
"accept-language": "^3.0.18", "accept-language": "^3.0.18",
"axios": "^0.26.0", "axios": "^0.27.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"body-parser": "^1.19.2", "body-parser": "^1.20.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
"dayjs": "^1.10.7", "dayjs": "^1.11.3",
"dompurify": "^2.3.6", "dompurify": "^2.3.8",
"email-templates": "^8.0.9", "email-templates": "^8.0.9",
"express": "^4.17.3", "express": "^4.18.1",
"express-oauth-server": "lesion/express-oauth-server#master", "express-oauth-server": "lesion/express-oauth-server#master",
"http-signature": "^1.3.6", "http-signature": "^1.3.6",
"ical.js": "^1.5.0", "ical.js": "^1.5.0",
@@ -49,55 +51,41 @@
"linkify-html": "^3.0.4", "linkify-html": "^3.0.4",
"linkifyjs": "3.0.5", "linkifyjs": "3.0.5",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mariadb": "^2.5.6", "mariadb": "^3.0.0",
"microformat-node": "^2.0.1", "microformat-node": "^2.0.1",
"minify-css-string": "^1.0.0", "minify-css-string": "^1.0.0",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"multer": "^1.4.3", "multer": "^1.4.5-lts.1",
"nuxt-edge": "^2.16.0-27305297.ab1c6cb4", "nuxt-edge": "^2.16.0-27358576.777a4b7f",
"pg": "^8.6.0", "pg": "^8.6.0",
"sequelize": "^6.17.0", "sequelize": "^6.20.1",
"sequelize-slugify": "^1.6.0", "sequelize-slugify": "^1.6.1",
"sharp": "^0.27.2", "sharp": "^0.27.2",
"sqlite3": "mapbox/node-sqlite3#918052b", "sqlite3": "^5.0.8",
"tiptap": "^1.32.0", "tiptap": "^1.32.0",
"tiptap-extensions": "^1.35.0", "tiptap-extensions": "^1.35.0",
"umzug": "^2.3.0", "umzug": "^2.3.0",
"v-calendar": "2.4.1", "v-calendar": "^2.4.1",
"vue": "^2.6.14", "vue": "^2.6.14",
"vue-i18n": "^8.26.7", "vue-i18n": "^8.26.7",
"vue-template-compiler": "^2.6.14", "vue-template-compiler": "^2.6.14",
"vuetify": "npm:@vuetify/nightly@dev", "vuetify": "npm:@vuetify/nightly@dev",
"winston": "^3.6.0", "winston": "^3.7.2",
"winston-daily-rotate-file": "^4.6.1", "winston-daily-rotate-file": "^4.7.1",
"yargs": "^17.2.0" "yargs": "^17.5.0"
}, },
"devDependencies": { "devDependencies": {
"@nuxtjs/vuetify": "^1.12.3", "@nuxtjs/vuetify": "^1.12.3",
"jest": "^27.5.1", "jest": "^28.1.0",
"less": "^4.1.1", "prettier": "^2.6.2",
"less-loader": "^7",
"prettier": "^2.3.0",
"pug": "^3.0.2", "pug": "^3.0.2",
"pug-plain-loader": "^1.1.0", "pug-plain-loader": "^1.1.0",
"sass": "^1.49.4", "sass": "^1.52.2",
"sequelize-cli": "^6.3.0", "sequelize-cli": "^6.3.0",
"supertest": "^6.2.2", "supertest": "^6.2.2",
"webpack": "4", "webpack": "4",
"webpack-cli": "^4.7.2" "webpack-cli": "^4.7.2"
}, },
"resolutions": {
"source-map-resolve": "0.6.0",
"lodash": "4.17.21",
"minimist": "1.2.5",
"jimp": "0.16.1",
"resize-img": "2.0.0",
"underscore": "1.13.1",
"postcss": "7.0.36",
"glob-parent": "5.1.2",
"chokidar": "3.5.2",
"core-js": "3.19.0"
},
"bin": { "bin": {
"gancio": "server/cli.js" "gancio": "server/cli.js"
}, },

View File

@@ -1,7 +1,9 @@
<template lang="pug"> <template lang="pug">
v-container.container.pa-0.pa-md-3 v-container.container.pa-0.pa-md-3
v-card v-card
v-tabs(v-model='selectedTab' show-arrows) v-alert(v-if='url!==settings.baseurl' outlined type='warning' color='red' show-icon :icon='mdiAlert')
span(v-html="$t('admin.wrong_domain_warning', { url, baseurl: settings.baseurl })")
v-tabs(v-model='selectedTab' show-arrows :next-icon='mdiChevronRight' :prev-icon='mdiChevronLeft')
//- SETTINGS //- SETTINGS
v-tab {{$t('common.settings')}} v-tab {{$t('common.settings')}}
@@ -24,6 +26,11 @@
v-tab-item v-tab-item
Places Places
//- Cohorts
v-tab {{$t('common.blobs')}}
v-tab-item
Cohorts
//- EVENTS //- EVENTS
v-tab v-tab
v-badge(:value='!!unconfirmedEvents.length' :content='unconfirmedEvents.length') {{$t('common.events')}} v-badge(:value='!!unconfirmedEvents.length' :content='unconfirmedEvents.length') {{$t('common.events')}}
@@ -49,32 +56,42 @@
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
import { mdiAlert, mdiChevronRight, mdiChevronLeft } from '@mdi/js'
import Settings from '@/components/admin/Settings'
export default { export default {
name: 'Admin', name: 'Admin',
components: { components: {
Settings,
Users: () => import(/* webpackChunkName: "admin" */'../components/admin/Users'), Users: () => import(/* webpackChunkName: "admin" */'../components/admin/Users'),
Events: () => import(/* webpackChunkName: "admin" */'../components/admin/Events'), Events: () => import(/* webpackChunkName: "admin" */'../components/admin/Events'),
Places: () => import(/* webpackChunkName: "admin" */'../components/admin/Places'), Places: () => import(/* webpackChunkName: "admin" */'../components/admin/Places'),
Settings: () => import(/* webpackChunkName: "admin" */'../components/admin/Settings'), Cohorts: () => import(/* webpackChunkName: "admin" */'../components/admin/Cohorts'),
Federation: () => import(/* webpackChunkName: "admin" */'../components/admin/Federation.vue'), Federation: () => import(/* webpackChunkName: "admin" */'../components/admin/Federation.vue'),
Moderation: () => import(/* webpackChunkName: "admin" */'../components/admin/Moderation.vue'), Moderation: () => import(/* webpackChunkName: "admin" */'../components/admin/Moderation.vue'),
Announcement: () => import(/* webpackChunkName: "admin" */'../components/admin/Announcement.vue'), Announcement: () => import(/* webpackChunkName: "admin" */'../components/admin/Announcement.vue'),
Theme: () => import(/* webpackChunkName: "admin" */'../components/admin/Theme.vue') Theme: () => import(/* webpackChunkName: "admin" */'../components/admin/Theme.vue')
}, },
middleware: ['auth'], middleware: ['auth'],
async asyncData ({ $axios, params, store }) { async asyncData ({ $axios, req }) {
let url
if (process.client) {
url = window.location.protocol + '//' + window.location.host
} else {
url = req.protocol + '://' + req.headers.host
}
try { try {
const users = await $axios.$get('/users') const users = await $axios.$get('/users')
const unconfirmedEvents = await $axios.$get('/event/unconfirmed') const unconfirmedEvents = await $axios.$get('/event/unconfirmed')
return { users, unconfirmedEvents, selectedTab: 0 } return { users, unconfirmedEvents, selectedTab: 0, url }
} catch (e) { } catch (e) {
console.error(e) return { users: [], unconfirmedEvents: [], selectedTab: 0, url }
return { users: [], unconfirmedEvents: [], selectedTab: 0 }
} }
}, },
data () { data () {
return { return {
mdiAlert, mdiChevronRight, mdiChevronLeft,
users: [],
description: '', description: '',
unconfirmedEvents: [], unconfirmedEvents: [],
selectedTab: 0 selectedTab: 0
@@ -100,7 +117,7 @@ export default {
this.loading = true this.loading = true
await this.$axios.$get(`/event/confirm/${id}`) await this.$axios.$get(`/event/confirm/${id}`)
this.loading = false this.loading = false
this.$root.$message('event.confirmed', { color: 'succes' }) this.$root.$message('event.confirmed', { color: 'success' })
this.unconfirmedEvents = this.unconfirmedEvents.filter(e => e.id !== id) this.unconfirmedEvents = this.unconfirmedEvents.filter(e => e.id !== id)
} }
} }

View File

@@ -1,80 +0,0 @@
<template lang="pug">
v-row
v-col.col-6
v-menu(v-model='startTimeMenu'
:close-on-content-click="false"
transition="slide-x-transition"
ref='startTimeMenu'
:return-value.sync="value.start"
offset-y
absolute
top
max-width="290px"
min-width="290px")
template(v-slot:activator='{ on }')
v-text-field(
:label="$t('event.from')"
prepend-icon='mdi-clock'
:rules="[$validators.required('event.from')]"
:value='value.start'
v-on='on'
clearable)
v-time-picker(
v-if='startTimeMenu'
:label="$t('event.from')"
format="24hr"
ref='time_start'
:allowed-minutes="[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]"
v-model='value.start'
@click:minute="selectTime('start')")
v-col.col-6
v-menu(v-model='endTimeMenu'
:close-on-content-click="false"
transition="slide-x-transition"
ref='endTimeMenu'
:return-value.sync="time.end"
offset-y
absolute
top
max-width="290px"
min-width="290px")
template(v-slot:activator='{ on }')
v-text-field(
prepend-icon='mdi-clock'
:label="$t('event.due')"
:value='value.end'
v-on='on'
clearable
readonly)
v-time-picker(
v-if='endTimeMenu'
:label="$t('event.due')"
format="24hr"
:allowed-minutes="[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]"
v-model='value.end'
@click:minute="selectTime('end')")
</template>
<script>
export default {
name: 'HourInput',
props: {
value: { type: Object, default: () => { } }
},
data () {
return {
// time: { start: this.value.start, end: this.value.end },
time: {},
startTimeMenu: false,
endTimeMenu: false
}
},
methods: {
selectTime (type) {
this.$refs[`${type}TimeMenu`].save(this.value[type])
this.$emit('input', this.value)
}
}
}
</script>

View File

@@ -1,102 +0,0 @@
<template lang="pug">
v-row
v-col(cols=12 md=6)
v-combobox(ref='place'
:rules="[$validators.required('common.where')]"
:label="$t('common.where')"
:hint="$t('event.where_description')"
:search-input.sync="placeName"
:prepend-icon='mdiMapMarker'
persistent-hint
:value="value.name"
:items="filteredPlaces"
no-filter
item-text='name'
@change='selectPlace')
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' v-text='mdiPlus' :aria-label='add'></v-icon> {{item.name}}
v-list-item-content(two-line v-else)
v-list-item-title(v-text='item.name')
v-list-item-subtitle(v-text='item.address')
v-col(cols=12 md=6)
v-text-field(ref='address'
:prepend-icon='mdiMap'
:disabled='disableAddress'
:rules="[ v => disableAddress ? true : $validators.required('common.address')(v)]"
:label="$t('common.address')"
@change="changeAddress"
:value="value.address")
</template>
<script>
import { mapState } from 'vuex'
import { mdiMap, mdiMapMarker, mdiPlus } from '@mdi/js'
export default {
name: 'WhereInput',
props: {
value: { type: Object, default: () => {} }
},
data () {
return {
mdiMap, mdiMapMarker, mdiPlus,
place: { },
placeName: '',
disableAddress: true
}
},
computed: {
...mapState(['places']),
filteredPlaces () {
if (!this.placeName) { return this.places }
const placeName = this.placeName.toLowerCase()
let nameMatch = false
const matches = this.places.filter(p => {
const tmpName = p.name.toLowerCase()
const tmpAddress = p.address.toLowerCase()
if (tmpName.includes(placeName)) {
if (tmpName === placeName) { nameMatch = true }
return true
}
if (tmpAddress.includes(placeName)) { return true }
return false
})
if (!nameMatch) {
matches.unshift({ create: true, name: this.placeName })
}
return matches
}
},
methods: {
selectPlace (p) {
if (!p) { return }
if (typeof p === 'object' && !p.create) {
this.place.name = p.name
this.place.address = p.address
this.disableAddress = true
} else { // this is a new place
this.place.name = p.name || p
// search for a place with the same name
const place = this.places.find(p => p.name === this.place.name)
if (place) {
this.place.address = place.address
this.disableAddress = true
} else {
this.place.address = ''
this.disableAddress = false
this.$refs.place.blur()
this.$refs.address.focus()
}
}
this.$emit('input', { ...this.place })
},
changeAddress (v) {
this.place.address = v
this.$emit('input', { ...this.place })
}
}
}
</script>

View File

@@ -51,8 +51,11 @@
v-combobox(v-model='event.tags' v-combobox(v-model='event.tags'
:prepend-icon="mdiTagMultiple" :prepend-icon="mdiTagMultiple"
chips small-chips multiple deletable-chips hide-no-data hide-selected persistent-hint chips small-chips multiple deletable-chips hide-no-data hide-selected persistent-hint
cache-items
@input.native='searchTags'
:delimiters="[',', ';']" :delimiters="[',', ';']"
:items="tags.map(t => t.tag)" :items="tags"
:menu-props="{ maxWidth: 400, eager: true }"
:label="$t('common.tags')") :label="$t('common.tags')")
template(v-slot:selection="{ item, on, attrs, selected, parent}") template(v-slot:selection="{ item, on, attrs, selected, parent}")
v-chip(v-bind="attrs" close :close-icon='mdiCloseCircle' @click:close='parent.selectItem(item)' v-chip(v-bind="attrs" close :close-icon='mdiCloseCircle' @click:close='parent.selectItem(item)'
@@ -66,25 +69,33 @@
</template> </template>
<script> <script>
import { mapActions, mapState } from 'vuex' import { mapState } from 'vuex'
import debounce from 'lodash/debounce'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { mdiFileImport, mdiFormatTitle, mdiTagMultiple, mdiCloseCircle } from '@mdi/js' import { mdiFileImport, mdiFormatTitle, mdiTagMultiple, mdiCloseCircle } from '@mdi/js'
import List from '@/components/List'
import Editor from '@/components/Editor'
import ImportDialog from '@/components/ImportDialog'
import MediaInput from '@/components/MediaInput'
import WhereInput from '@/components/WhereInput'
import DateInput from '@/components/DateInput'
export default { export default {
name: 'NewEvent', name: 'NewEvent',
components: { components: {
List: () => import(/* webpackChunkName: "add" */'@/components/List'), List,
Editor: () => import(/* webpackChunkName: "add" */'@/components/Editor'), Editor,
ImportDialog: () => import(/* webpackChunkName: "add" */'./ImportDialog.vue'), ImportDialog,
MediaInput: () => import(/* webpackChunkName: "add" */'./MediaInput.vue'), MediaInput,
WhereInput: () => import(/* webpackChunkName: "add" */'./WhereInput.vue'), WhereInput,
DateInput: () => import(/* webpackChunkName: "add" */'./DateInput.vue') DateInput
}, },
validate ({ store }) { validate ({ store }) {
return (store.state.auth.loggedIn || store.state.settings.allow_anon_event) return (store.state.auth.loggedIn || store.state.settings.allow_anon_event)
}, },
async asyncData ({ params, $axios, error, store }) { async asyncData ({ params, $axios, error }) {
if (params.edit) { if (params.edit) {
const data = { event: { place: {}, media: [] } } const data = { event: { place: {}, media: [] } }
data.id = params.edit data.id = params.edit
@@ -101,8 +112,8 @@ export default {
data.event.place.address = event.place.address || '' data.event.place.address = event.place.address || ''
data.date = { data.date = {
recurrent: event.recurrent, recurrent: event.recurrent,
from: new Date(dayjs.unix(event.start_datetime)), from: dayjs.unix(event.start_datetime).toDate(),
due: new Date(dayjs.unix(event.end_datetime)), due: dayjs.unix(event.end_datetime).toDate(),
multidate: event.multidate, multidate: event.multidate,
fromHour: true, fromHour: true,
dueHour: true dueHour: true
@@ -118,8 +129,8 @@ export default {
return {} return {}
}, },
data () { data () {
const month = dayjs().month() + 1 const month = dayjs.tz().month() + 1
const year = dayjs().year() const year = dayjs.tz().year()
return { return {
mdiFileImport, mdiFormatTitle, mdiTagMultiple, mdiCloseCircle, mdiFileImport, mdiFormatTitle, mdiTagMultiple, mdiCloseCircle,
valid: false, valid: false,
@@ -131,6 +142,7 @@ export default {
tags: [], tags: [],
media: [] media: []
}, },
tags: [],
page: { month, year }, page: { month, year },
fileList: [], fileList: [],
id: null, id: null,
@@ -145,9 +157,20 @@ export default {
title: `${this.settings.title} - ${this.$t('common.add_event')}` title: `${this.settings.title} - ${this.$t('common.add_event')}`
} }
}, },
computed: mapState(['tags', 'places', 'settings']), computed: {
...mapState(['settings']),
filteredTags () {
if (!this.tagName) { return this.tags.slice(0, 10).map(t => t.tag) }
const tagName = this.tagName.trim().toLowerCase()
return this.tags.filter(t => t.tag.toLowerCase().includes(tagName)).map(t => t.tag)
}
},
methods: { methods: {
...mapActions(['updateMeta']), searchTags: debounce( async function(ev) {
const search = ev.target.value
if (!search) return
this.tags = await this.$axios.$get(`/tag?search=${search}`)
}, 100),
eventImported (event) { eventImported (event) {
this.event = Object.assign(this.event, event) this.event = Object.assign(this.event, event)
this.$refs.where.selectPlace({ name: event.place.name, create: true }) this.$refs.where.selectPlace({ name: event.place.name, create: true })
@@ -165,7 +188,9 @@ export default {
if (!this.$refs.form.validate()) { if (!this.$refs.form.validate()) {
this.$nextTick(() => { this.$nextTick(() => {
const el = document.querySelector('.v-input.error--text:first-of-type') const el = document.querySelector('.v-input.error--text:first-of-type')
el.scrollIntoView() if (el) {
el.scrollIntoView(false)
}
}) })
return return
} }
@@ -177,12 +202,15 @@ export default {
if (this.event.media.length) { if (this.event.media.length) {
formData.append('image', this.event.media[0].image) formData.append('image', this.event.media[0].image)
formData.append('image_url', this.event.media[0].url) // formData.append('image_url', this.event.media[0].url)
formData.append('image_name', this.event.media[0].name) formData.append('image_name', this.event.media[0].name)
formData.append('image_focalpoint', this.event.media[0].focalpoint) formData.append('image_focalpoint', this.event.media[0].focalpoint)
} }
formData.append('title', this.event.title) formData.append('title', this.event.title)
if (this.event.place.id) {
formData.append('place_id', this.event.place.id)
}
formData.append('place_name', this.event.place.name) formData.append('place_name', this.event.place.name)
formData.append('place_address', this.event.place.address) formData.append('place_address', this.event.place.address)
formData.append('description', this.event.description) formData.append('description', this.event.description)
@@ -200,7 +228,6 @@ export default {
} else { } else {
await this.$axios.$post('/event', formData) await this.$axios.$post('/event', formData)
} }
this.updateMeta()
this.$router.push('/') this.$router.push('/')
this.$nextTick(() => { this.$nextTick(() => {
this.$root.$message(this.$auth.loggedIn ? (this.edit ? 'event.saved' : 'event.added') : 'event.added_anon', { color: 'success' }) this.$root.$message(this.$auth.loggedIn ? (this.edit ? 'event.saved' : 'event.added') : 'event.added_anon', { color: 'success' })

View File

@@ -33,7 +33,7 @@ export default {
// <iframe src='http://localhost:13120/embed/1' class='embedded_gancio'></iframe> // <iframe src='http://localhost:13120/embed/1' class='embedded_gancio'></iframe>
</script> </script>
<style lang='less'> <style lang='scss'>
.embed_event { .embed_event {
display: flex; display: flex;
transition: margin .1s; transition: margin .1s;

View File

@@ -2,6 +2,7 @@
v-container#event.pa-0.pa-sm-2 v-container#event.pa-0.pa-sm-2
//- EVENT PAGE //- EVENT PAGE
//- gancio supports microformats (http://microformats.org/wiki/h-event) //- gancio supports microformats (http://microformats.org/wiki/h-event)
//- and microdata https://schema.org/Event
v-card.h-event(itemscope itemtype="https://schema.org/Event") v-card.h-event(itemscope itemtype="https://schema.org/Event")
v-card-actions v-card-actions
//- admin controls //- admin controls
@@ -9,41 +10,33 @@ v-container#event.pa-0.pa-sm-2
v-card-text v-card-text
v-row v-row
v-col.col-12.col-lg-8 v-col.col-12.col-md-8
//- fake image to use u-featured in h-event microformat MyPicture(v-if='hasMedia' :event='event')
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' itemprop='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-col.col-12.col-md-4
v-card(outlined) v-card(outlined)
v-card-text v-card-text
v-icon.float-right(v-if='event.parentId' color='success' v-text='mdiRepeat') v-icon.float-right(v-if='event.parentId' color='success' v-text='mdiRepeat')
.title.text-h5 .title.text-h5.mb-5
b.p-name(itemprop="name") {{event.title}} strong.p-name.text--primary(itemprop="name") {{event.title}}
time.dt-start.text-h6(:datetime='event.start_datetime|unixFormat("YYYY-MM-DD HH:mm")' itemprop="startDate" :content="event.start_datetime|unixFormat('YYYY-MM-DDTHH:mm')") time.dt-start.text-h6(:datetime='event.start_datetime|unixFormat("YYYY-MM-DD HH:mm")' itemprop="startDate" :content="event.start_datetime|unixFormat('YYYY-MM-DDTHH:mm')")
v-icon(v-text='mdiCalendar') v-icon(v-text='mdiCalendar')
b.ml-2 {{event|when}} strong.ml-2 {{event|when}}
.d-none.dt-end(itemprop="endDate" :content="event.end_datetime|unixFormat('YYYY-MM-DDTHH:mm')") {{event.end_datetime|unixFormat('YYYY-MM-DD HH:mm')}} .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}} div.text-subtitle-1.mb-5 {{event.start_datetime|from}}
small(v-if='event.parentId') ({{event|recurrentDetail}}) small(v-if='event.parentId') ({{event|recurrentDetail}})
.text-h6.p-location(itemprop="location" itemscope itemtype="https://schema.org/Place") .text-h6.p-location.h-adr(itemprop="location" itemscope itemtype="https://schema.org/Place")
v-icon(v-text='mdiMapMarker') v-icon(v-text='mdiMapMarker')
b.vcard.ml-2(itemprop="name") {{event.place && event.place.name}} b.vcard.ml-2.p-name(itemprop="name") {{event.place && event.place.name}}
.text-subtitle-1.adr(itemprop='address') {{event.place && event.place.address}} .text-subtitle-1.p-street-address(itemprop='address') {{event.place && event.place.address}}
//- tags, hashtags //- tags, hashtags
v-card-text(v-if='event.tags.length') v-card-text.pt-0(v-if='event.tags && event.tags.length')
v-chip.p-category.ml-1.mt-3(v-for='tag in event.tags' color='primary' v-chip.p-category.ml-1.mt-3(v-for='tag in event.tags' color='primary'
outlined :key='tag') outlined :key='tag' :to='`/tag/${tag}`') {{tag}}
span(v-text='tag')
//- info & actions //- info & actions
v-toolbar v-toolbar
@@ -55,6 +48,9 @@ v-container#event.pa-0.pa-sm-2
v-btn.ml-2(large icon :title="$t('common.add_to_calendar')" color='primary' :aria-label="$t('common.add_to_calendar')" 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`') :href='`/api/event/${event.slug || event.id}.ics`')
v-icon(v-text='mdiCalendarExport') v-icon(v-text='mdiCalendarExport')
v-btn.ml-2(v-if='hasMedia' large icon :title="$t('event.download_flyer')" color='primary' :aria-label="$t('event.download_flyer')"
:href='event | mediaURL')
v-icon(v-text='mdiFileDownloadOutline')
.p-description.text-body-1.pa-3.rounded(v-if='hasMedia && event.description' itemprop='description' v-html='event.description') .p-description.text-body-1.pa-3.rounded(v-if='hasMedia && event.description' itemprop='description' v-html='event.description')
@@ -133,20 +129,25 @@ import { mapState } from 'vuex'
import get from 'lodash/get' import get from 'lodash/get'
import moment from 'dayjs' import moment from 'dayjs'
import clipboard from '../../assets/clipboard' import clipboard from '../../assets/clipboard'
const htmlToText = require('html-to-text') import MyPicture from '~/components/MyPicture'
import EventAdmin from '@/components/eventAdmin'
import EmbedEvent from '@/components/embedEvent'
const { htmlToText } = require('html-to-text')
import { mdiArrowLeft, mdiArrowRight, mdiDotsVertical, mdiCodeTags, mdiClose, import { mdiArrowLeft, mdiArrowRight, mdiDotsVertical, mdiCodeTags, mdiClose,
mdiEye, mdiEyeOff, mdiDelete, mdiRepeat, mdiLock, mdiEye, mdiEyeOff, mdiDelete, mdiRepeat, mdiLock, mdiFileDownloadOutline,
mdiCalendarExport, mdiCalendar, mdiContentCopy, mdiMapMarker } from '@mdi/js' mdiCalendarExport, mdiCalendar, mdiContentCopy, mdiMapMarker } from '@mdi/js'
export default { export default {
name: 'Event', name: 'Event',
mixins: [clipboard], mixins: [clipboard],
components: { components: {
EventAdmin: () => import(/* webpackChunkName: "event" */'./eventAdmin'), EventAdmin,
EmbedEvent: () => import(/* webpackChunkName: "event" */'./embedEvent'), EmbedEvent,
MyPicture
}, },
async asyncData ({ $axios, params, error, store }) { async asyncData ({ $axios, params, error }) {
try { try {
const event = await $axios.$get(`/event/${params.slug}`) const event = await $axios.$get(`/event/${params.slug}`)
return { event } return { event }
@@ -156,7 +157,7 @@ export default {
}, },
data () { data () {
return { return {
mdiArrowLeft, mdiArrowRight, mdiDotsVertical, mdiCodeTags, mdiCalendarExport, mdiCalendar, mdiArrowLeft, mdiArrowRight, mdiDotsVertical, mdiCodeTags, mdiCalendarExport, mdiCalendar, mdiFileDownloadOutline,
mdiMapMarker, mdiContentCopy, mdiClose, mdiDelete, mdiEye, mdiEyeOff, mdiRepeat, mdiLock, mdiMapMarker, mdiContentCopy, mdiClose, mdiDelete, mdiEye, mdiEyeOff, mdiRepeat, mdiLock,
currentAttachment: 0, currentAttachment: 0,
event: {}, event: {},
@@ -169,7 +170,7 @@ export default {
if (!this.event) { if (!this.event) {
return {} return {}
} }
const tags_feed = this.event.tags.map(tag => ({ const tags_feed = this.event.tags && this.event.tags.map(tag => ({
rel: 'alternate', rel: 'alternate',
type: 'application/rss+xml', type: 'application/rss+xml',
title: `${this.settings.title} events tagged ${tag}`, title: `${this.settings.title} events tagged ${tag}`,
@@ -247,7 +248,7 @@ export default {
return this.event.media && this.event.media.length return this.event.media && this.event.media.length
}, },
plainDescription () { plainDescription () {
return htmlToText.fromString(this.event.description.replace('\n', '').slice(0, 1000)) return htmlToText(this.event.description && this.event.description.replace('\n', '').slice(0, 1000))
}, },
currentAttachmentLabel () { currentAttachmentLabel () {
return get(this.selectedResource, `data.attachment[${this.currentAttachment}].name`, '') return get(this.selectedResource, `data.attachment[${this.currentAttachment}].name`, '')
@@ -331,10 +332,3 @@ export default {
} }
} }
</script> </script>
<style scoped>
.main_image {
margin: 0 auto;
border-radius: 5px;
transition: max-height 0.2s;
}
</style>

View File

@@ -11,7 +11,7 @@
Search( Search(
:filters='filters' :filters='filters'
@update='f => filters = f') @update='f => filters = f')
v-tabs(v-model='type' show-arrows) v-tabs(v-model='type' show-arrows :next-icon='mdiChevronRight' :prev-icon='mdiChevronLeft')
//- TOFIX //- TOFIX
//- v-tab {{$t('common.email')}} //- v-tab {{$t('common.email')}}
@@ -86,7 +86,7 @@ import { mapState } from 'vuex'
import FollowMe from '../components/FollowMe' import FollowMe from '../components/FollowMe'
import Search from '@/components/Search' import Search from '@/components/Search'
import clipboard from '../assets/clipboard' import clipboard from '../assets/clipboard'
import { mdiContentCopy } from '@mdi/js' import { mdiContentCopy, mdiChevronRight, mdiChevronLeft } from '@mdi/js'
export default { export default {
name: 'Exports', name: 'Exports',
@@ -104,7 +104,7 @@ export default {
}, },
data ({ $store }) { data ({ $store }) {
return { return {
mdiContentCopy, mdiContentCopy, mdiChevronLeft, mdiChevronRight,
type: 'rss', type: 'rss',
notification: { email: '' }, notification: { email: '' },
list: { list: {

31
pages/g/_cohort.vue Normal file
View File

@@ -0,0 +1,31 @@
<template>
<v-container id='home' fluid>
<h1 class='d-block text-h3 font-weight-black text-center align-center text-uppercase mt-10 mb-12 mx-auto w-100 text-underline'><u>{{cohort}}</u></h1>
<!-- Events -->
<div class='mb-2 mt-1 pl-1 pl-sm-2' id="events">
<Event :event='event' v-for='(event, idx) in events' :lazy='idx>2' :key='event.id'></Event>
</div>
</v-container>
</template>
<script>
import Event from '@/components/Event'
export default {
name: 'Tag',
components: { Event },
async asyncData ({ $axios, params, error }) {
try {
const cohort = params.cohort
const events = await $axios.$get(`/cohorts/${cohort}`)
return { events, cohort }
} catch (e) {
console.error(e)
error({ statusCode: 400, message: 'Error!' })
}
}
}
</script>

View File

@@ -6,38 +6,36 @@
Announcement(v-for='announcement in announcements' :key='`a_${announcement.id}`' :announcement='announcement') Announcement(v-for='announcement in announcements' :key='`a_${announcement.id}`' :announcement='announcement')
//- Calendar and search bar //- Calendar and search bar
v-row.pt-0.pt-sm-2.pl-0.pl-sm-2 v-row.ma-2
#calh.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-0.ma-0
//- this is needed as v-calendar does not support SSR //- this is needed as v-calendar does not support SSR
//- https://github.com/nathanreyes/v-calendar/issues/336 //- https://github.com/nathanreyes/v-calendar/issues/336
client-only(placeholder='Calendar unavailable without js') client-only(placeholder='Loading...')
Calendar(@dayclick='dayChange' @monthchange='monthChange' :events='filteredEvents') Calendar(@dayclick='dayChange' @monthchange='monthChange' :events='events')
.col.pt-0.pt-md-2 .col.pt-0.pt-md-2.mt-4.ma-md-0.pb-0
Search(:filters='filters' @update='updateFilters') //- v-btn(to='/search' color='primary' ) {{$t('common.search')}}
v-chip(v-if='selectedDay' close :close-icon='mdiCloseCircle' @click:close='dayChange()') {{selectedDay}} v-form(to='/search' action='/search' method='GET')
v-text-field(name='search' :label='$t("common.search")' outlined rounded hide-details :append-icon='mdiMagnify')
//- Events //- Events
#events.mb-2.mt-1.pl-1.pl-sm-2 #events.mb-2.mt-1.pl-1.pl-sm-2
Event(:event='event' @destroy='destroy' v-for='(event, idx) in visibleEvents' :lazy='idx>2' :key='event.id' @tagclick='tagClick' @placeclick='placeClick') Event(:event='event' @destroy='destroy' v-for='(event, idx) in visibleEvents' :lazy='idx>2' :key='event.id')
</template> </template>
<script> <script>
import { mapState, mapActions } from 'vuex' import { mapState } from 'vuex'
import intersection from 'lodash/intersection'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import Event from '@/components/Event' import Event from '@/components/Event'
import Announcement from '@/components/Announcement' import Announcement from '@/components/Announcement'
import Search from '@/components/Search'
import Calendar from '@/components/Calendar' import Calendar from '@/components/Calendar'
import { mdiCloseCircle } from '@mdi/js' import { mdiMagnify } from '@mdi/js'
export default { export default {
name: 'Index', name: 'Index',
components: { Event, Search, Announcement, Calendar }, components: { Event, Announcement, Calendar },
middleware: 'setup', middleware: 'setup',
async asyncData ({ params, $api, store }) { async asyncData ({ $api }) {
const events = await $api.getEvents({ const events = await $api.getEvents({
start: dayjs().startOf('month').unix(), start: dayjs().startOf('month').unix(),
end: null, end: null,
@@ -45,13 +43,13 @@ export default {
}) })
return { events } return { events }
}, },
data ({ $store }) { data () {
return { return {
mdiCloseCircle, mdiMagnify,
first: true, first: true,
isCurrentMonth: true, isCurrentMonth: true,
now: dayjs().unix(), now: dayjs().unix(),
date: dayjs().format('YYYY-MM-DD'), date: dayjs.tz().format('YYYY-MM-DD'),
events: [], events: [],
start: dayjs().startOf('month').unix(), start: dayjs().startOf('month').unix(),
end: null, end: null,
@@ -74,49 +72,22 @@ export default {
] ]
} }
}, },
computed: { computed: {
...mapState(['settings', 'announcements', 'filters']), ...mapState(['settings', 'announcements']),
filteredEvents () {
let events = this.events
if (!this.filters.places.length && !this.filters.tags.length) {
if (this.filters.show_recurrent) {
return this.events
}
events = events.filter(e => !e.parentId)
}
return events.filter(e => {
// check tags intersection
if (this.filters.tags.length) {
const ret = intersection(this.filters.tags, e.tags)
if (!ret.length) { return false }
}
// check if place is in filtered places
if (this.filters.places.length && !this.filters.places.includes(e.place.id)) {
return false
}
return true
})
},
visibleEvents () { visibleEvents () {
const now = dayjs().unix() const now = dayjs().unix()
if (this.selectedDay) { if (this.selectedDay) {
const min = dayjs(this.selectedDay).startOf('day').unix() const min = dayjs(this.selectedDay).startOf('day').unix()
const max = dayjs(this.selectedDay).endOf('day').unix() const max = dayjs(this.selectedDay).endOf('day').unix()
return this.filteredEvents.filter(e => (e.start_datetime < max && e.start_datetime > min)) return this.events.filter(e => (e.start_datetime <= max && e.start_datetime >= min))
} else if (this.isCurrentMonth) { } else if (this.isCurrentMonth) {
return this.filteredEvents.filter(e => e.end_datetime ? e.end_datetime > now : e.start_datetime + 2 * 60 * 60 > now) return this.events.filter(e => e.end_datetime ? e.end_datetime > now : e.start_datetime + 2 * 60 * 60 > now)
} else { } else {
return this.filteredEvents return this.events
} }
} }
}, },
methods: { methods: {
// onIntersect (isIntersecting, eventId) {
// this.intersecting[eventId] = isIntersecting
// },
...mapActions(['setFilters']),
destroy (id) { destroy (id) {
this.events = this.events.filter(e => e.id !== id) this.events = this.events.filter(e => e.id !== id)
}, },
@@ -131,20 +102,6 @@ export default {
this.$nuxt.$loading.finish() this.$nuxt.$loading.finish()
}) })
}, },
placeClick (place_id) {
if (this.filters.places.includes(place_id)) {
this.setFilters({ ...this.filters, places: this.filters.places.filter(p_id => p_id !== place_id) })
} else {
this.setFilters({ ...this.filters, places: [].concat(this.filters.places, place_id) })
}
},
tagClick (tag) {
if (this.filters.tags.includes(tag)) {
this.setFilters({ ...this.filters, tags: this.filters.tags.filter(t => t !== tag) })
} else {
this.setFilters({ ...this.filters, tags: [].concat(this.filters.tags, tag) })
}
},
monthChange ({ year, month }) { monthChange ({ year, month }) {
// avoid first time monthChange event (onload) // avoid first time monthChange event (onload)
if (this.first) { if (this.first) {
@@ -158,24 +115,20 @@ export default {
this.selectedDay = null this.selectedDay = null
// check if current month is selected // check if current month is selected
if (month - 1 === dayjs().month() && year === dayjs().year()) { if (month - 1 === dayjs.tz().month() && year === dayjs.tz().year()) {
this.isCurrentMonth = true this.isCurrentMonth = true
this.start = dayjs().startOf('month').unix() this.start = dayjs().startOf('month').unix()
this.date = dayjs().format('YYYY-MM-DD') this.date = dayjs.tz().format('YYYY-MM-DD')
} else { } else {
this.isCurrentMonth = false this.isCurrentMonth = false
this.date = '' this.date = ''
this.start = dayjs().year(year).month(month - 1).startOf('month').unix() // .startOf('week').unix() this.start = dayjs().year(year).month(month - 1).startOf('month').unix() // .startOf('week').unix()
} }
// TODO: check if calendar view is double
this.end = dayjs().year(year).month(month).endOf('month').unix() // .endOf('week').unix() this.end = dayjs().year(year).month(month).endOf('month').unix() // .endOf('week').unix()
this.updateEvents() this.updateEvents()
}, },
updateFilters (filters) {
this.setFilters(filters)
},
dayChange (day) { dayChange (day) {
this.selectedDay = day ? dayjs(day).format('YYYY-MM-DD') : null this.selectedDay = day ? dayjs.tz(day).format('YYYY-MM-DD') : null
} }
} }
} }

30
pages/p/_place.vue Normal file
View File

@@ -0,0 +1,30 @@
<template>
<v-container id='home' fluid>
<h1 class='d-block text-h4 font-weight-black text-center text-uppercase mt-5 mx-auto w-100 text-underline'><u>{{place.name}}</u></h1>
<span class="d-block text-subtitle text-center w-100 mb-14">{{place.address}}</span>
<!-- Events -->
<div class="mb-2 mt-1 pl-1 pl-sm-2" id="events">
<Event :event='event' v-for='(event, idx) in events' :lazy='idx>2' :key='event.id'></Event>
</div>
</v-container>
</template>
<script>
import Event from '@/components/Event'
export default {
name: 'Tag',
components: { Event },
asyncData ({ $axios, params, error }) {
try {
const place = params.place
return $axios.$get(`/place/${place}/events`)
} catch (e) {
error({ statusCode: 400, message: 'Error!' })
}
}
}
</script>

42
pages/search.vue Normal file
View File

@@ -0,0 +1,42 @@
<template lang="pug">
v-container#home(fluid)
v-form.ma-5(to='/search' action='/search' method='GET')
v-text-field(name='search' :label='$t("common.search")' :value='$route.query.search' hide-details outlined rounded :append-icon='mdiMagnify')
//- Events
#events.mb-2.mt-1.pl-1.pl-sm-2
Event(:event='event' @destroy='destroy' v-for='(event, idx) in events' :lazy='idx>2' :key='event.id')
</template>
<script>
import { mapState } from 'vuex'
import dayjs from 'dayjs'
import Event from '@/components/Event'
import Announcement from '@/components/Announcement'
import Calendar from '@/components/Calendar'
import { mdiMagnify } from '@mdi/js'
export default {
name: 'Index',
components: { Event, Announcement, Calendar },
data () {
return {
mdiMagnify,
events: [],
start: dayjs().startOf('month').unix(),
end: null,
}
},
async fetch () {
const search = this.$route.query.search
this.events = await this.$axios.$get(`/event/search?search=${search}`)
},
computed: mapState(['settings']),
methods: {
destroy (id) {
this.events = this.events.filter(e => e.id !== id)
}
}
}
</script>

View File

@@ -1,41 +0,0 @@
<template lang="pug">
v-container
v-card-title.d-block.text-h5.text-center(v-text="$t('setup.completed')")
v-card-text(v-html="$t('setup.completed_description', user)")
v-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(v-text='mdiArrowRight')
</template>
<script>
import { mdiArrowRight, mdiAlert } from '@mdi/js'
export default {
data () {
return {
mdiArrowRight, mdiAlert,
loading: false,
user: {
email: 'admin',
password: ''
}
}
},
methods: {
next () {
window.location='/admin'
},
async start (user) {
this.user = { ...user }
this.loading = true
try {
await this.$axios.$get('/ping')
this.loading = false
} catch (e) {
setTimeout(() => this.start(user), 1000)
}
}
}
}
</script>

View File

@@ -1,5 +1,4 @@
<template lang="pug"> <template lang="pug">
v-container.pa-6 v-container.pa-6
h2.mb-2.text-center Gancio Setup h2.mb-2.text-center Gancio Setup
v-stepper.grey.lighten-5(v-model='step') v-stepper.grey.lighten-5(v-model='step')
@@ -16,17 +15,12 @@
v-stepper-content(step='2') v-stepper-content(step='2')
Settings(setup, @complete='configCompleted') Settings(setup, @complete='configCompleted')
v-stepper-content(step='3') v-stepper-content(step='3')
Completed(ref='completed') Completed(ref='completed' :isHttp='isHttp')
</template> </template>
<script> <script>
import DbStep from './DbStep' import DbStep from '@/components/DbStep'
import Settings from '../../components/admin/Settings' import Settings from '@/components/admin/Settings'
import Completed from './Completed' import Completed from '@/components/Completed'
export default { export default {
components: { DbStep, Settings, Completed }, components: { DbStep, Settings, Completed },
@@ -36,9 +30,10 @@ export default {
title: 'Setup', title: 'Setup',
}, },
auth: false, auth: false,
asyncData ({ params }) { asyncData ({ params, req }) {
const protocol = process.client ? window.location.protocol : req.protocol + ':'
return { return {
isHttp: protocol === 'http:',
dbdone: !!Number(params.db), dbdone: !!Number(params.db),
config: { config: {
db: { db: {

30
pages/tag/_tag.vue Normal file
View File

@@ -0,0 +1,30 @@
<template>
<v-container id='home' fluid>
<h1 class='d-block text-h3 font-weight-black text-center align-center text-uppercase mt-5 mb-16 mx-auto w-100 text-underline'><u>{{tag}}</u></h1>
<!-- Events -->
<div class="mb-2 mt-1 pl-1 pl-sm-2" id="events">
<Event :event='event' v-for='(event, idx) in events' :lazy='idx>2' :key='event.id'></Event>
</div>
</v-container>
</template>
<script>
import Event from '@/components/Event'
export default {
name: 'Tag',
components: { Event },
async asyncData ({ $axios, params, error }) {
try {
const tag = params.tag
const events = await $axios.$get(`/events?tags=${tag}`)
return { events, tag }
} catch (e) {
error({ statusCode: 400, message: 'Error!' })
}
}
}
</script>

View File

@@ -10,7 +10,7 @@ export default ({ $axios }, inject) => {
* end_datetime: unix_timestamp * end_datetime: unix_timestamp
* tags: [tag, list], * tags: [tag, list],
* places: [place_id], * places: [place_id],
* limit: (default ∞) * max: (default ∞)
* } * }
* *
*/ */
@@ -22,7 +22,8 @@ export default ({ $axios }, inject) => {
end: params.end, end: params.end,
places: params.places && params.places.join(','), places: params.places && params.places.join(','),
tags: params.tags && params.tags.join(','), tags: params.tags && params.tags.join(','),
show_recurrent: !!params.show_recurrent show_recurrent: !!params.show_recurrent,
max: params.maxs
} }
}) })
return events.map(e => Object.freeze(e)) return events.map(e => Object.freeze(e))

View File

@@ -7,12 +7,15 @@ import localizedFormat from 'dayjs/plugin/localizedFormat'
import 'dayjs/locale/it' import 'dayjs/locale/it'
import 'dayjs/locale/en'
import 'dayjs/locale/es' import 'dayjs/locale/es'
import 'dayjs/locale/ca' import 'dayjs/locale/ca'
import 'dayjs/locale/pl' import 'dayjs/locale/pl'
import 'dayjs/locale/eu' import 'dayjs/locale/eu'
import 'dayjs/locale/nb' import 'dayjs/locale/nb'
import 'dayjs/locale/fr' import 'dayjs/locale/fr'
import 'dayjs/locale/de'
import 'dayjs/locale/gl'
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
dayjs.extend(utc) dayjs.extend(utc)
@@ -23,25 +26,27 @@ export default ({ app, store }) => {
// set timezone to instance_timezone!! // set timezone to instance_timezone!!
// to show local time relative to event's place // to show local time relative to event's place
// not where in the world I'm looking at the page from // not where in the world I'm looking at the page from
dayjs.tz.setDefault(store.state.settings.instance_timezone) const instance_timezone = store.state.settings.instance_timezone
dayjs.locale(store.state.locale) const locale = store.state.locale
dayjs.tz.setDefault(instance_timezone)
dayjs.locale(locale)
// replace links with anchors // replace links with anchors
// TODO: remove fb tracking id? // TODO: remove fb tracking id?
Vue.filter('linkify', value => value.replace(/(https?:\/\/([^\s]+))/g, '<a href="$1">$2</a>')) Vue.filter('linkify', value => value.replace(/(https?:\/\/([^\s]+))/g, '<a href="$1">$2</a>'))
Vue.filter('url2host', url => url.match(/^https?:\/\/(.[^/:]+)/i)[1]) Vue.filter('url2host', url => url.match(/^https?:\/\/(.[^/:]+)/i)[1])
Vue.filter('datetime', value => dayjs(value).locale(store.state.locale).format('ddd, D MMMM HH:mm')) Vue.filter('datetime', value => dayjs.tz(value).locale(locale).format('ddd, D MMMM HH:mm'))
Vue.filter('dateFormat', (value, format) => dayjs(value).format(format)) Vue.filter('dateFormat', (value, format) => dayjs.tz(value).format(format))
Vue.filter('unixFormat', (timestamp, format) => dayjs.unix(timestamp).format(format)) Vue.filter('unixFormat', (timestamp, format) => dayjs.unix(timestamp).tz(instance_timezone).format(format))
// shown in mobile homepage // shown in mobile homepage
Vue.filter('day', value => dayjs.unix(value).locale(store.state.locale).format('dddd, D MMM')) Vue.filter('day', value => dayjs.unix(value).tz(instance_timezone).locale(store.state.locale).format('dddd, D MMM'))
Vue.filter('mediaURL', (event, type) => { Vue.filter('mediaURL', (event, type, format = '.jpg') => {
if (event.media && event.media.length) { if (event.media && event.media.length) {
if (type === 'alt') { if (type === 'alt') {
return event.media[0].name return event.media[0].name
} else { } else {
return store.state.settings.baseurl + '/media/' + (type === 'thumb' ? 'thumb/' : '') + event.media[0].url return store.state.settings.baseurl + '/media/' + (type === 'thumb' ? 'thumb/' : '') + event.media[0].url.replace(/.jpg$/, format)
} }
} else if (type !== 'alt') { } else if (type !== 'alt') {
return store.state.settings.baseurl + '/media/' + (type === 'thumb' ? 'thumb/' : '') + 'logo.svg' return store.state.settings.baseurl + '/media/' + (type === 'thumb' ? 'thumb/' : '') + 'logo.svg'
@@ -49,16 +54,16 @@ export default ({ app, store }) => {
return '' return ''
}) })
Vue.filter('from', timestamp => dayjs.unix(timestamp).fromNow()) Vue.filter('from', timestamp => dayjs.unix(timestamp).tz(instance_timezone).fromNow())
Vue.filter('recurrentDetail', event => { Vue.filter('recurrentDetail', event => {
const parent = event.parent const parent = event.parent
const { frequency, type } = parent.recurrent const { frequency, type } = parent.recurrent
let recurrent let recurrent
if (frequency === '1w' || frequency === '2w') { if (frequency === '1w' || frequency === '2w') {
recurrent = app.i18n.t(`event.recurrent_${frequency}_days`, { days: dayjs.unix(parent.start_datetime).format('dddd') }) recurrent = app.i18n.t(`event.recurrent_${frequency}_days`, { days: dayjs.unix(parent.start_datetime).tz(instance_timezone).format('dddd') })
} else if (frequency === '1m' || frequency === '2m') { } else if (frequency === '1m' || frequency === '2m') {
const d = type === 'ordinal' ? dayjs.unix(parent.start_datetime).date() : dayjs.unix(parent.start_datetime).format('dddd') const d = type === 'ordinal' ? dayjs.unix(parent.start_datetime).date() : dayjs.unix(parent.start_datetime).tz(instance_timezone).format('dddd')
if (type === 'ordinal') { if (type === 'ordinal') {
recurrent = app.i18n.t(`event.recurrent_${frequency}_days`, { days: d }) recurrent = app.i18n.t(`event.recurrent_${frequency}_days`, { days: d })
} else { } else {
@@ -70,8 +75,8 @@ export default ({ app, store }) => {
}) })
Vue.filter('when', (event) => { Vue.filter('when', (event) => {
const start = dayjs.unix(event.start_datetime) const start = dayjs.unix(event.start_datetime).tz(instance_timezone)
const end = dayjs.unix(event.end_datetime) const end = dayjs.unix(event.end_datetime).tz(instance_timezone)
// const normal = `${start.format('dddd, D MMMM (HH:mm-')}${end.format('HH:mm) ')}` // const normal = `${start.format('dddd, D MMMM (HH:mm-')}${end.format('HH:mm) ')}`
// // recurrent event // // recurrent event
@@ -90,10 +95,10 @@ export default ({ app, store }) => {
// multidate // multidate
if (event.multidate) { if (event.multidate) {
return `${start.format('ddd, D MMM HH:mm')} - ${end.format('ddd, D MMM')}` return `${start.format('ddd, D MMM HH:mm')} - ${end.format('ddd, D MMM HH:mm')}`
} }
// normal event // normal event
return start.format('ddd, D MMMM HH:mm') return `${start.format('ddd, D MMM HH:mm')} - ${end.format('HH:mm')}`
}) })
} }

View File

@@ -0,0 +1,191 @@
const Cohort = require('../models/cohort')
const Filter = require('../models/filter')
const Event = require('../models/event')
const Tag = require('../models/tag')
const Place = require('../models/place')
const log = require('../../log')
const dayjs = require('dayjs')
// const { sequelize } = require('../models/index')
const { Op, Sequelize } = require('sequelize')
const cohortController = {
async getAll (req, res) {
const withFilters = req.query.withFilters
let cohorts
if (withFilters) {
cohorts = await Cohort.findAll({ include: [Filter] })
} else {
cohorts = await Cohort.findAll()
}
return res.json(cohorts)
},
// return events from cohort
async getEvents (req, res) {
const name = req.params.name
const cohort = await Cohort.findOne({ where: { name } })
if (!cohort) {
return res.sendStatus(404)
}
const filters = await Filter.findAll({ where: { cohortId: cohort.id } })
const start = dayjs().unix()
const where = {
// do not include parent recurrent event
recurrent: null,
// confirmed event only
is_visible: true,
// [Op.or]: {
start_datetime: { [Op.gte]: start },
// end_datetime: { [Op.gte]: start }
// }
}
// if (!show_recurrent) {
// where.parentId = null
// }
// if (end) {
// where.start_datetime = { [Op.lte]: end }
// }
const replacements = []
const ors = []
filters.forEach(f => {
if (f.tags && f.tags.length) {
const tags = Sequelize.fn('EXISTS', Sequelize.literal('SELECT 1 FROM event_tags WHERE "event_tags"."eventId"="event".id AND "tagTag" in (?)'))
replacements.push(f.tags)
if (f.places && f.places.length) {
ors.push({ [Op.and]: [ { placeId: f.places.map(p => p.id) },tags] })
} else {
ors.push(tags)
}
} else if (f.places && f.places.length) {
ors.push({ placeId: f.places.map(p => p.id) })
}
})
// if (tags && places) {
// where[Op.or] = {
// placeId: places ? places.split(',') : [],
// // '$tags.tag$': Sequelize.literal(`EXISTS (SELECT 1 FROM event_tags WHERE tagTag in ( ${Sequelize.QueryInterface.escape(tags)} ) )`)
// }
// } else if (tags) {
// where[Op.and] = Sequelize.literal(`EXISTS (SELECT 1 FROM event_tags WHERE event_tags.eventId=event.id AND tagTag in (?))`)
// replacements.push(tags)
// } else if (places) {
// where.placeId = places.split(',')
// }
if (ors.length) {
where[Op.or] = ors
}
const events = await Event.findAll({
logging: console.log,
where,
attributes: {
exclude: ['likes', 'boost', 'userId', 'is_visible', 'createdAt', 'updatedAt', 'description', 'resources']
},
order: ['start_datetime'],
include: [
// { model: Resource, required: false, attributes: ['id'] },
{
model: Tag,
order: [Sequelize.literal('(SELECT COUNT("tagTag") FROM event_tags WHERE tagTag = tag) DESC')],
attributes: ['tag'],
through: { attributes: [] }
},
{ model: Place, required: true, attributes: ['id', 'name', 'address'] }
],
// limit: max,
replacements
}).catch(e => {
log.error('[EVENT]', e)
return []
})
const ret = events.map(e => {
e = e.get()
e.tags = e.tags ? e.tags.map(t => t && t.tag) : []
return e
})
return res.json(ret)
},
async add (req, res) {
const cohortDetail = {
name: req.body.name,
isActor: true,
isTop: true
}
// TODO: validation
log.info('Create cohort: ' + req.body.name)
const cohort = await Cohort.create(cohortDetail)
res.json(cohort)
},
async remove (req, res) {
const cohort_id = req.params.id
log.info('Remove cohort', cohort_id)
try {
const cohort = await Cohort.findByPk(cohort_id)
await cohort.destroy()
res.sendStatus(200)
} catch (e) {
log.error('Remove cohort failed:', e)
res.sendStatus(404)
}
},
async getFilters (req, res) {
const cohortId = req.params.cohort_id
const filters = await Filter.findAll({ where: { cohortId } })
return res.json(filters)
},
async addFilter (req, res) {
const cohortId = req.body.cohortId
const tags = req.body.tags
const places = req.body.places
try {
const filter = await Filter.create({ cohortId, tags, places })
return res.json(filter)
} catch (e) {
log.error(String(e))
return res.status(500)
}
},
async removeFilter (req, res) {
const filter_id = req.params.id
log.info('Remove filter', filter_id)
try {
const filter = await Filter.findByPk(filter_id)
await filter.destroy()
res.sendStatus(200)
} catch (e) {
log.error('Remove filter failed:', e)
res.sendStatus(404)
}
},
}
module.exports = cohortController

View File

@@ -8,7 +8,6 @@ const linkifyHtml = require('linkify-html')
const Sequelize = require('sequelize') const Sequelize = require('sequelize')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const helpers = require('../../helpers') const helpers = require('../../helpers')
const settingsController = require('./settings')
const Event = require('../models/event') const Event = require('../models/event')
const Resource = require('../models/resource') const Resource = require('../models/resource')
@@ -23,31 +22,110 @@ const log = require('../../log')
const eventController = { const eventController = {
async _getMeta () { async searchMeta (req, res) {
const search = req.query.search
const places = await Place.findAll({ const places = await Place.findAll({
order: [[Sequelize.literal('weigth'), 'DESC']], order: [[Sequelize.col('w'), 'DESC']],
attributes: { where: {
include: [[Sequelize.fn('count', Sequelize.col('events.placeId')), 'weigth']], [Op.or]: [
exclude: ['createdAt', 'updatedAt'] { name: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), 'LIKE', '%' + search + '%' )},
{ address: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('address')), 'LIKE', '%' + search + '%')},
]
}, },
attributes: [['name', 'label'], 'address', 'id', [Sequelize.cast(Sequelize.fn('COUNT', Sequelize.col('events.placeId')),'INTEGER'), 'w']],
include: [{ model: Event, where: { is_visible: true }, required: true, attributes: [] }], include: [{ model: Event, where: { is_visible: true }, required: true, attributes: [] }],
group: ['place.id'] group: ['place.id'],
raw: true
}) })
const tags = await Tag.findAll({ const tags = await Tag.findAll({
order: [[Sequelize.literal('w'), 'DESC']], order: [[Sequelize.col('w'), 'DESC']],
attributes: { where: {
include: [[Sequelize.fn('COUNT', Sequelize.col('tag.tag')), 'w']] tag: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('tag')), 'LIKE', '%' + search + '%'),
}, },
attributes: [['tag','label'], [Sequelize.cast(Sequelize.fn('COUNT', Sequelize.col('tag.tag')), 'INTEGER'), 'w']],
include: [{ model: Event, where: { is_visible: true }, attributes: [], through: { attributes: [] }, required: true }], include: [{ model: Event, where: { is_visible: true }, attributes: [], through: { attributes: [] }, required: true }],
group: ['tag.tag'] group: ['tag.tag'],
raw: true
}) })
return { places, tags } const ret = places.map(p => {
p.type = 'place'
return p
}).concat(tags.map(t => {
t.type = 'tag'
return t
})).sort( (a, b) => b.w - a.w).slice(0, 10)
return res.json(ret)
}, },
async getMeta (req, res) {
res.json(await eventController._getMeta()) async search (req, res) {
const search = req.query.search.trim().toLocaleLowerCase()
const show_recurrent = req.query.show_recurrent || false
const end = req.query.end
const replacements = []
const where = {
// do not include parent recurrent event
recurrent: null,
// confirmed event only
is_visible: true,
}
if (!show_recurrent) {
where.parentId = null
}
if (end) {
where.start_datetime = { [Op.lte]: end }
}
if (search) {
replacements.push(search)
where[Op.or] =
[
{ title: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('title')), 'LIKE', '%' + search + '%') },
Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), 'LIKE', '%' + search + '%'),
Sequelize.fn('EXISTS', Sequelize.literal('SELECT 1 FROM event_tags WHERE "event_tags"."eventId"="event".id AND "tagTag" = ?'))
]
}
const events = await Event.findAll({
where,
attributes: {
exclude: ['likes', 'boost', 'userId', 'is_visible', 'createdAt', 'updatedAt', 'description', 'resources']
},
order: [['start_datetime', 'DESC']],
include: [
{
model: Tag,
order: [Sequelize.literal('(SELECT COUNT("tagTag") FROM event_tags WHERE tagTag = tag) DESC')],
attributes: ['tag'],
through: { attributes: [] }
},
{ model: Place, required: true, attributes: ['id', 'name', 'address'] }
],
replacements,
limit: 30,
}).catch(e => {
log.error('[EVENT]', e)
return res.json([])
})
const ret = events.map(e => {
e = e.get()
e.tags = e.tags ? e.tags.map(t => t && t.tag) : []
return e
})
return res.json(ret)
}, },
async getNotifications (event, action) { async getNotifications (event, action) {
@@ -75,14 +153,7 @@ const eventController = {
const notifications = await Notification.findAll({ where: { action }, include: [Event] }) const notifications = await Notification.findAll({ where: { action }, include: [Event] })
// get notification that matches with selected event // get notification that matches with selected event
const ret = notifications.filter(notification => match(event, notification.filters)) return notifications.filter(notification => match(event, notification.filters))
return ret
},
async updatePlace (req, res) {
const place = await Place.findByPk(req.body.id)
await place.update(req.body)
res.json(place)
}, },
async _get(slug) { async _get(slug) {
@@ -290,8 +361,8 @@ const eventController = {
res.sendStatus(200) res.sendStatus(200)
}, },
async isAnonEventAllowed (req, res, next) { async isAnonEventAllowed (_req, res, next) {
if (!res.locals.settings.allow_anon_event) { if (!res.locals.settings.allow_anon_event && !res.locals.user) {
return res.sendStatus(403) return res.sendStatus(403)
} }
next() next()
@@ -308,16 +379,33 @@ const eventController = {
const body = req.body const body = req.body
const recurrent = body.recurrent ? JSON.parse(body.recurrent) : null const recurrent = body.recurrent ? JSON.parse(body.recurrent) : null
const required_fields = [ 'title', 'place_name', 'start_datetime'] const required_fields = [ 'title', 'start_datetime']
const missing_field = required_fields.find(required_field => !body[required_field]) let missing_field = required_fields.find(required_field => !body[required_field])
if (missing_field) { if (missing_field) {
log.warn(`${missing_field} is required`) log.warn(`${missing_field} required`)
return res.status(400).send(`${missing_field} is required`) return res.status(400).send(`${missing_field} required`)
}
// find or create the place
let place
if (body.place_id) {
place = await Place.findByPk(body.place_id)
} else {
place = await Place.findOne({ where: { name: body.place_name.trim() }})
if (!place) {
if (!body.place_address || !body.place_name) {
return res.status(400).send(`place_id or place_name and place_address required`)
}
place = await Place.create({
name: body.place_name,
address: body.place_address
})
}
} }
const eventDetails = { const eventDetails = {
title: body.title, title: body.title,
// remove html tags // sanitize and linkify html
description: helpers.sanitizeHTML(linkifyHtml(body.description || '')), description: helpers.sanitizeHTML(linkifyHtml(body.description || '')),
multidate: body.multidate, multidate: body.multidate,
start_datetime: body.start_datetime, start_datetime: body.start_datetime,
@@ -328,17 +416,16 @@ const eventController = {
} }
if (req.file || body.image_url) { if (req.file || body.image_url) {
let url if (!req.file && body.image_url) {
if (req.file) { req.file = await helpers.getImageFromURL(body.image_url)
url = req.file.filename
} else {
url = await helpers.getImageFromURL(body.image_url)
} }
let focalpoint = body.image_focalpoint ? body.image_focalpoint.split(',') : ['0', '0'] let focalpoint = body.image_focalpoint ? body.image_focalpoint.split(',') : ['0', '0']
focalpoint = [parseFloat(focalpoint[0]).toFixed(2), parseFloat(focalpoint[1]).toFixed(2)] focalpoint = [parseFloat(focalpoint[0]).toFixed(2), parseFloat(focalpoint[1]).toFixed(2)]
eventDetails.media = [{ eventDetails.media = [{
url, url: req.file.filename,
height: req.file.height,
width: req.file.width,
name: body.image_name || body.title || '', name: body.image_name || body.title || '',
focalpoint: [parseFloat(focalpoint[0]), parseFloat(focalpoint[1])] focalpoint: [parseFloat(focalpoint[0]), parseFloat(focalpoint[1])]
}] }]
@@ -346,24 +433,16 @@ const eventController = {
eventDetails.media = [] eventDetails.media = []
} }
const event = await Event.create(eventDetails) let event = await Event.create(eventDetails)
const [place] = await Place.findOrCreate({
where: { name: body.place_name },
defaults: {
address: body.place_address
}
})
await event.setPlace(place) await event.setPlace(place)
event.place = place
// create/assign tags // create/assign tags
if (body.tags) { if (body.tags) {
body.tags = body.tags.map(t => t.trim())
await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true }) await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true })
const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } }) const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } })
await event.addTags(tags) await event.addTags(tags)
event.tags = tags
} }
// associate user to event and reverse // associate user to event and reverse
@@ -372,6 +451,9 @@ const eventController = {
await event.setUser(res.locals.user) await event.setUser(res.locals.user)
} }
event = event.get()
event.tags = body.tags
event.place = place
// return created event to the client // return created event to the client
res.json(event) res.json(event)
@@ -405,47 +487,44 @@ const eventController = {
const recurrent = body.recurrent ? JSON.parse(body.recurrent) : null const recurrent = body.recurrent ? JSON.parse(body.recurrent) : null
const eventDetails = { const eventDetails = {
title: body.title, title: body.title || event.title,
// remove html tags // sanitize and linkify html
description: helpers.sanitizeHTML(linkifyHtml(body.description, { target: '_blank' })), description: helpers.sanitizeHTML(linkifyHtml(body.description, { target: '_blank' })) || event.description,
multidate: body.multidate, multidate: body.multidate,
start_datetime: body.start_datetime, start_datetime: body.start_datetime,
end_datetime: body.end_datetime, end_datetime: body.end_datetime,
recurrent recurrent
} }
// remove old media in case a new one is uploaded
if ((req.file || /^https?:\/\//.test(body.image_url)) && !event.recurrent && event.media && event.media.length) { if ((req.file || /^https?:\/\//.test(body.image_url)) && !event.recurrent && event.media && event.media.length) {
try {
const old_path = path.resolve(config.upload_path, event.media[0].url) const old_path = path.resolve(config.upload_path, event.media[0].url)
const old_thumb_path = path.resolve(config.upload_path, 'thumb', event.media[0].url) const old_thumb_path = path.resolve(config.upload_path, 'thumb', event.media[0].url)
try {
fs.unlinkSync(old_path) fs.unlinkSync(old_path)
fs.unlinkSync(old_thumb_path) fs.unlinkSync(old_thumb_path)
} catch (e) { } catch (e) {
log.info(e.toString()) log.info(e.toString())
} }
} }
let url
if (req.file) { // modify associated media only if a new file is uploaded or remote image_url is used
url = req.file.filename if (req.file || (body.image_url && /^https?:\/\//.test(body.image_url))) {
} else if (body.image_url) { if (body.image_url) {
if (/^https?:\/\//.test(body.image_url)) { req.file = await helpers.getImageFromURL(body.image_url)
url = await helpers.getImageFromURL(body.image_url)
} else {
url = body.image_url
}
} }
if (url && !event.recurrent) {
const focalpoint = body.image_focalpoint ? body.image_focalpoint.split(',') : ['0', '0'] const focalpoint = body.image_focalpoint ? body.image_focalpoint.split(',') : ['0', '0']
eventDetails.media = [{ eventDetails.media = [{
url, url: req.file.filename,
name: body.image_name || '', height: req.file.height,
width: req.file.width,
name: body.image_name || body.title || '',
focalpoint: [parseFloat(focalpoint[0].slice(0, 6)), parseFloat(focalpoint[1].slice(0, 6))] focalpoint: [parseFloat(focalpoint[0].slice(0, 6)), parseFloat(focalpoint[1].slice(0, 6))]
}] }]
} else { } else if (!body.image) {
eventDetails.media = [] eventDetails.media = []
} }
await event.update(eventDetails) await event.update(eventDetails)
const [place] = await Place.findOrCreate({ const [place] = await Place.findOrCreate({
where: { name: body.place_name }, where: { name: body.place_name },
@@ -481,9 +560,9 @@ const eventController = {
// check if event is mine (or user is admin) // check if event is mine (or user is admin)
if (event && (res.locals.user.is_admin || res.locals.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) { if (event.media && event.media.length && !event.recurrent) {
try {
const old_path = path.join(config.upload_path, event.media[0].url) 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) const old_thumb_path = path.join(config.upload_path, 'thumb', event.media[0].url)
try {
fs.unlinkSync(old_thumb_path) fs.unlinkSync(old_thumb_path)
fs.unlinkSync(old_path) fs.unlinkSync(old_path)
} catch (e) { } catch (e) {
@@ -523,22 +602,22 @@ const eventController = {
if (!show_recurrent) { if (!show_recurrent) {
where.parentId = null where.parentId = null
} }
if (end) { if (end) {
where.start_datetime = { [Op.lte]: end } where.start_datetime = { [Op.lte]: end }
} }
const replacements = []
if (tags && places) { if (tags && places) {
where[Op.or] = { where[Op.or] = {
placeId: places ? places.split(',') : [], placeId: places ? places.split(',') : [],
'$tags.tag$': tags.split(',') // '$tags.tag$': Sequelize.literal(`EXISTS (SELECT 1 FROM event_tags WHERE tagTag in ( ${Sequelize.QueryInterface.escape(tags)} ) )`)
} }
} } else if (tags) {
// where[Op.and] = Sequelize.literal(`EXISTS (SELECT 1 FROM event_tags WHERE eventId=event.id AND tagTag in (?))`)
if (tags) { where[Op.and] = Sequelize.fn('EXISTS', Sequelize.literal('SELECT 1 FROM event_tags WHERE "event_tags"."eventId"="event".id AND "tagTag" in (?)'))
where['$tags.tag$'] = tags.split(',') replacements.push(tags)
} } else if (places) {
if (places) {
where.placeId = places.split(',') where.placeId = places.split(',')
} }
@@ -554,12 +633,12 @@ const eventController = {
model: Tag, model: Tag,
order: [Sequelize.literal('(SELECT COUNT("tagTag") FROM event_tags WHERE tagTag = tag) DESC')], order: [Sequelize.literal('(SELECT COUNT("tagTag") FROM event_tags WHERE tagTag = tag) DESC')],
attributes: ['tag'], attributes: ['tag'],
required: !!tags,
through: { attributes: [] } through: { attributes: [] }
}, },
{ model: Place, required: true, attributes: ['id', 'name', 'address'] } { model: Place, required: true, attributes: ['id', 'name', 'address'] }
], ],
limit: max limit: max,
replacements
}).catch(e => { }).catch(e => {
log.error('[EVENT]', e) log.error('[EVENT]', e)
return [] return []

View File

@@ -2,6 +2,7 @@ const Event = require('../models/event')
const Place = require('../models/place') const Place = require('../models/place')
const Tag = require('../models/tag') const Tag = require('../models/tag')
const { htmlToText } = require('html-to-text')
const { Op, literal } = require('sequelize') const { Op, literal } = require('sequelize')
const moment = require('dayjs') const moment = require('dayjs')
const ics = require('ics') const ics = require('ics')
@@ -68,7 +69,7 @@ const exportController = {
} }
}, },
feed (req, res, events) { feed (_req, res, events) {
const settings = res.locals.settings const settings = res.locals.settings
res.type('application/rss+xml; charset=UTF-8') res.type('application/rss+xml; charset=UTF-8')
res.render('feed/rss.pug', { events, settings, moment }) res.render('feed/rss.pug', { events, settings, moment })
@@ -79,7 +80,7 @@ const exportController = {
* @param {*} events array of events from sequelize * @param {*} events array of events from sequelize
* @param {*} alarms https://github.com/adamgibbons/ics#attributes (alarms) * @param {*} alarms https://github.com/adamgibbons/ics#attributes (alarms)
*/ */
ics (req, res, events, alarms = []) { ics (_req, res, events, alarms = []) {
const settings = res.locals.settings const settings = res.locals.settings
const eventsMap = events.map(e => { const eventsMap = events.map(e => {
const tmpStart = moment.unix(e.start_datetime) const tmpStart = moment.unix(e.start_datetime)
@@ -88,13 +89,14 @@ const exportController = {
const end = tmpEnd.utc(true).format('YYYY-M-D-H-m').split('-').map(Number) const end = tmpEnd.utc(true).format('YYYY-M-D-H-m').split('-').map(Number)
return { return {
start, start,
// startOutputType: 'utc',
end, end,
// endOutputType: 'utc',
title: `[${settings.title}] ${e.title}`, title: `[${settings.title}] ${e.title}`,
description: e.description, description: htmlToText(e.description),
htmlContent: e.description,
location: `${e.place.name} - ${e.place.address}`, location: `${e.place.name} - ${e.place.address}`,
url: `${settings.baseurl}/event/${e.slug || e.id}`, url: `${settings.baseurl}/event/${e.slug || e.id}`,
status: 'CONFIRMED',
categories: e.tags.map(t => t.tag),
alarms alarms
} }
}) })

View File

@@ -137,8 +137,7 @@ const oauthController = {
code.userId = user.id code.userId = user.id
code.clientId = client.id code.clientId = client.id
code.expiresAt = dayjs(code.expiresAt).toDate() code.expiresAt = dayjs(code.expiresAt).toDate()
const ret = await OAuthCode.create(code) return OAuthCode.create(code)
return ret
}, },
// TODO // TODO

View File

@@ -0,0 +1,59 @@
const dayjs = require('dayjs')
const Place = require('../models/place')
const Event = require('../models/event')
const eventController = require('./event')
const log = require('../../log')
const { Op, where, col, fn, cast } = require('sequelize')
module.exports = {
async getEvents (req, res) {
const name = req.params.placeName
const place = await Place.findOne({ where: { name }})
if (!place) {
log.warn(`Place ${name} not found`)
return res.sendStatus(404)
}
const start = dayjs().unix()
const events = await eventController._select({ start, places: `${place.id}`, show_recurrent: true})
return res.json({ events, place })
},
async updatePlace (req, res) {
const place = await Place.findByPk(req.body.id)
await place.update(req.body)
res.json(place)
},
async getAll (_req, res) {
const places = await Place.findAll({
order: [[cast(fn('COUNT', col('events.placeId')),'INTEGER'), 'DESC']],
include: [{ model: Event, where: { is_visible: true }, required: true, attributes: [] }],
group: ['place.id'],
raw: true
})
return res.json(places)
},
async get (req, res) {
const search = req.query.search.toLocaleLowerCase()
const places = await Place.findAll({
order: [[cast(fn('COUNT', col('events.placeId')),'INTEGER'), 'DESC']],
where: {
[Op.or]: [
{ name: where(fn('LOWER', col('name')), 'LIKE', '%' + search + '%' )},
{ address: where(fn('LOWER', col('address')), 'LIKE', '%' + search + '%')},
]
},
attributes: ['name', 'address', 'id'],
include: [{ model: Event, where: { is_visible: true }, required: true, attributes: [] }],
group: ['place.id'],
raw: true
})
// TOFIX: don't know why limit does not work
return res.json(places.slice(0, 10))
}
}

View File

@@ -5,7 +5,6 @@ const crypto = require('crypto')
const { promisify } = require('util') const { promisify } = require('util')
const sharp = require('sharp') const sharp = require('sharp')
const config = require('../../config') const config = require('../../config')
const pkg = require('../../../package.json')
const generateKeyPair = promisify(crypto.generateKeyPair) const generateKeyPair = promisify(crypto.generateKeyPair)
const log = require('../../log') const log = require('../../log')
const locales = require('../../../locales/index') const locales = require('../../../locales/index')
@@ -42,7 +41,7 @@ const defaultSettings = {
{ href: '/about', label: 'about' } { href: '/about', label: 'about' }
], ],
admin_email: config.admin_email || '', admin_email: config.admin_email || '',
smtp: config.smtp || false smtp: config.smtp || {}
} }
/** /**
@@ -185,7 +184,7 @@ const settingsController = {
return sharp(uploadedPath) return sharp(uploadedPath)
.resize(400) .resize(400)
.png({ quality: 90 }) .png({ quality: 90 })
.toFile(baseImgPath + '.png', (err, info) => { .toFile(baseImgPath + '.png', (err) => {
if (err) { if (err) {
log.error('[LOGO] ' + err) log.error('[LOGO] ' + err)
} }

View File

@@ -0,0 +1,47 @@
const dayjs = require('dayjs')
const Tag = require('../models/tag')
const Event = require('../models/event')
const eventController = require('./event')
const Sequelize = require('sequelize')
module.exports = {
// async getEvents (req, res) {
// const name = req.params.placeName
// const place = await Place.findOne({ where: { name }})
// if (!place) {
// log.warn(`Place ${name} not found`)
// return res.sendStatus(404)
// }
// const start = dayjs().unix()
// const events = await eventController._select({ start, places: `${place.id}`, show_recurrent: true})
// return res.json({ events, place })
// },
async get (req, res) {
const search = req.query.search
console.error(search)
const tags = await Tag.findAll({
order: [[Sequelize.fn('COUNT', Sequelize.col('tag.tag')), 'DESC']],
attributes: ['tag'],
where: {
tag: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('tag')), 'LIKE', '%' + search + '%'),
},
include: [{ model: Event, where: { is_visible: true }, attributes: [], through: { attributes: [] }, required: true }],
group: ['tag.tag'],
limit: 10,
subQuery:false
})
return res.json(tags.map(t => t.tag))
}
// async getPlaces (req, res) {
// const search = req.params.search
// const places = await Place.findAll({ where: {
// [Op.or]: [
// { name: }
// ]
// }})
// }
}

View File

@@ -123,7 +123,12 @@ const userController = {
async remove (req, res) { async remove (req, res) {
try { try {
const user = await User.findByPk(req.params.id) let user
if (res.locals.user.is_admin && req.params.id) {
user = await User.findByPk(req.params.id)
} else {
user = await User.findByPk(res.locals.user.id)
}
await user.destroy() await user.destroy()
log.warn(`User ${user.email} removed!`) log.warn(`User ${user.email} removed!`)
res.sendStatus(200) res.sendStatus(200)

View File

@@ -23,6 +23,8 @@ if (config.status !== 'READY') {
const { isAuth, isAdmin } = require('./auth') const { isAuth, isAdmin } = require('./auth')
const eventController = require('./controller/event') const eventController = require('./controller/event')
const placeController = require('./controller/place')
const tagController = require('./controller/tag')
const settingsController = require('./controller/settings') const settingsController = require('./controller/settings')
const exportController = require('./controller/export') const exportController = require('./controller/export')
const userController = require('./controller/user') const userController = require('./controller/user')
@@ -31,6 +33,7 @@ if (config.status !== 'READY') {
const resourceController = require('./controller/resource') const resourceController = require('./controller/resource')
const oauthController = require('./controller/oauth') const oauthController = require('./controller/oauth')
const announceController = require('./controller/announce') const announceController = require('./controller/announce')
const cohortController = require('./controller/cohort')
const helpers = require('../helpers') const helpers = require('../helpers')
const storage = require('./storage') const storage = require('./storage')
const upload = multer({ storage }) const upload = multer({ storage })
@@ -72,14 +75,11 @@ if (config.status !== 'READY') {
// delete user // delete user
api.delete('/user/:id', isAdmin, userController.remove) api.delete('/user/:id', isAdmin, userController.remove)
api.delete('/user', isAdmin, userController.remove) api.delete('/user', isAuth, userController.remove)
// get all users // get all users
api.get('/users', isAdmin, userController.getAll) api.get('/users', isAdmin, userController.getAll)
// update a place (modify address..)
api.put('/place', isAdmin, eventController.updatePlace)
/** /**
* Get events * Get events
* @category Event * @category Event
@@ -120,6 +120,8 @@ if (config.status !== 'READY') {
// allow anyone to add an event (anon event has to be confirmed, TODO: flood protection) // allow anyone to add an event (anon event has to be confirmed, TODO: flood protection)
api.post('/event', eventController.isAnonEventAllowed, upload.single('image'), eventController.add) api.post('/event', eventController.isAnonEventAllowed, upload.single('image'), eventController.add)
api.get('/event/search', eventController.search)
api.put('/event', isAuth, upload.single('image'), eventController.update) api.put('/event', isAuth, upload.single('image'), eventController.update)
api.get('/event/import', isAuth, helpers.importURL) api.get('/event/import', isAuth, helpers.importURL)
@@ -127,7 +129,7 @@ if (config.status !== 'READY') {
api.delete('/event/:id', isAuth, eventController.remove) api.delete('/event/:id', isAuth, eventController.remove)
// get tags/places // get tags/places
api.get('/event/meta', eventController.getMeta) api.get('/event/meta', eventController.searchMeta)
// get unconfirmed events // get unconfirmed events
api.get('/event/unconfirmed', isAdmin, eventController.getUnconfirmed) api.get('/event/unconfirmed', isAdmin, eventController.getUnconfirmed)
@@ -150,6 +152,15 @@ if (config.status !== 'READY') {
// export events (rss/ics) // export events (rss/ics)
api.get('/export/:type', cors, exportController.export) api.get('/export/:type', cors, exportController.export)
api.get('/place/:placeName/events', cors, placeController.getEvents)
api.get('/place/all', isAdmin, placeController.getAll)
api.get('/place', cors, placeController.get)
api.put('/place', isAdmin, placeController.updatePlace)
api.get('/tag', cors, tagController.get)
// - FEDIVERSE INSTANCES, MODERATION, RESOURCES
api.get('/instances', isAdmin, instanceController.getAll) api.get('/instances', isAdmin, instanceController.getAll)
api.get('/instances/:instance_domain', isAdmin, instanceController.get) api.get('/instances/:instance_domain', isAdmin, instanceController.get)
api.post('/instances/toggle_block', isAdmin, instanceController.toggleBlock) api.post('/instances/toggle_block', isAdmin, instanceController.toggleBlock)
@@ -164,16 +175,25 @@ if (config.status !== 'READY') {
api.put('/announcements/:announce_id', isAdmin, announceController.update) api.put('/announcements/:announce_id', isAdmin, announceController.update)
api.delete('/announcements/:announce_id', isAdmin, announceController.remove) api.delete('/announcements/:announce_id', isAdmin, announceController.remove)
// - COHORT
api.get('/cohorts/:name', cohortController.getEvents)
api.get('/cohorts', cohortController.getAll)
api.post('/cohorts', isAdmin, cohortController.add)
api.delete('/cohort/:id', isAdmin, cohortController.remove)
api.get('/filter/:cohort_id', isAdmin, cohortController.getFilters)
api.post('/filter', isAdmin, cohortController.addFilter)
api.delete('/filter/:id', isAdmin, cohortController.removeFilter)
// OAUTH // OAUTH
api.get('/clients', isAuth, oauthController.getClients) api.get('/clients', isAuth, oauthController.getClients)
api.get('/client/:client_id', isAuth, oauthController.getClient) api.get('/client/:client_id', isAuth, oauthController.getClient)
api.post('/client', oauthController.createClient) api.post('/client', oauthController.createClient)
} }
api.use((req, res) => res.sendStatus(404)) api.use((_req, res) => res.sendStatus(404))
// Handle 500 // Handle 500
api.use((error, req, res, next) => { api.use((error, _req, res, _next) => {
log.error('[API ERROR]', error) log.error('[API ERROR]', error)
res.status(500).send('500: Internal Server Error') res.status(500).send('500: Internal Server Error')
}) })

View File

@@ -9,7 +9,7 @@ const locales = require('../../locales')
const mail = { const mail = {
send (addresses, template, locals, locale) { send (addresses, template, locals, locale) {
locale = locale || settingsController.settings.instance_locale locale = locale || settingsController.settings.instance_locale
if (process.env.NODE_ENV === 'production' && (!settingsController.settings.admin_email || !settingsController.settings.smtp)) { if (process.env.NODE_ENV === 'production' && (!settingsController.settings.admin_email || !settingsController.settings.smtp || !settingsController.settings.smtp.user)) {
log.error(`Cannot send any email: SMTP Email configuration not completed!`) log.error(`Cannot send any email: SMTP Email configuration not completed!`)
return return
} }

View File

@@ -0,0 +1,27 @@
const { Model, DataTypes } = require('sequelize')
const sequelize = require('./index').sequelize
class Cohort extends Model {}
Cohort.init({
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
name: {
type: DataTypes.STRING,
unique: true,
index: true,
allowNull: false
},
isActor: {
type: DataTypes.BOOLEAN
},
isTop: {
type: DataTypes.BOOLEAN
}
}, { sequelize, modelName: 'cohort', timestamps: false })
module.exports = Cohort

View File

@@ -1,5 +1,4 @@
const config = require('../../config') const config = require('../../config')
const moment = require('dayjs')
const { htmlToText } = require('html-to-text') const { htmlToText } = require('html-to-text')
const { Model, DataTypes } = require('sequelize') const { Model, DataTypes } = require('sequelize')
@@ -14,9 +13,12 @@ const Place = require('./place')
const User = require('./user') const User = require('./user')
const Tag = require('./tag') const Tag = require('./tag')
const utc = require('dayjs/plugin/utc')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const timezone = require('dayjs/plugin/timezone')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone)
class Event extends Model {} class Event extends Model {}
@@ -76,7 +78,7 @@ Event.prototype.toAP = function (username, locale, to = []) {
const plainDescription = htmlToText(this.description && this.description.replace('\n', '').slice(0, 1000)) const plainDescription = htmlToText(this.description && this.description.replace('\n', '').slice(0, 1000))
const content = ` const content = `
📍 ${this.place && this.place.name} 📍 ${this.place && this.place.name}
📅 ${moment.unix(this.start_datetime).locale(locale).format('dddd, D MMMM (HH:mm)')} 📅 ${dayjs.unix(this.start_datetime).tz().locale(locale).format('dddd, D MMMM (HH:mm)')}
${plainDescription} ${plainDescription}
` `
@@ -99,8 +101,8 @@ Event.prototype.toAP = function (username, locale, to = []) {
name: this.title, name: this.title,
url: `${config.baseurl}/event/${this.slug || this.id}`, url: `${config.baseurl}/event/${this.slug || this.id}`,
type: 'Event', type: 'Event',
startTime: moment.unix(this.start_datetime).locale(locale).format(), startTime: dayjs.unix(this.start_datetime).tz().locale(locale).format(),
endTime: this.end_datetime ? moment.unix(this.end_datetime).locale(locale).format() : null, endTime: this.end_datetime ? dayjs.unix(this.end_datetime).tz().locale(locale).format() : null,
location: { location: {
name: this.place.name, name: this.place.name,
address: this.place.address address: this.place.address

View File

@@ -0,0 +1,24 @@
const { Model, DataTypes } = require('sequelize')
const Cohort = require('./cohort')
const sequelize = require('./index').sequelize
class Filter extends Model {}
Filter.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
tags: {
type: DataTypes.JSON,
},
places: {
type: DataTypes.JSON,
}
}, { sequelize, modelName: 'filter', timestamps: false })
Filter.belongsTo(Cohort)
Cohort.hasMany(Filter)
module.exports = Filter

View File

@@ -15,6 +15,17 @@ const db = {
connect (dbConf = config.db) { connect (dbConf = config.db) {
log.debug(`Connecting to DB: ${JSON.stringify(dbConf)}`) log.debug(`Connecting to DB: ${JSON.stringify(dbConf)}`)
dbConf.dialectOptions = { autoJsonMap: false } dbConf.dialectOptions = { autoJsonMap: false }
if (dbConf.dialect === 'sqlite') {
dbConf.retry = {
match: [
Sequelize.ConnectionError,
Sequelize.ConnectionTimedOutError,
Sequelize.TimeoutError,
/Deadlock/i,
/SQLITE_BUSY/],
max: 15
}
}
db.sequelize = new Sequelize(dbConf) db.sequelize = new Sequelize(dbConf)
return db.sequelize.authenticate() return db.sequelize.authenticate()
}, },
@@ -39,7 +50,7 @@ const db = {
path: path.resolve(__dirname, '..', '..', 'migrations') path: path.resolve(__dirname, '..', '..', 'migrations')
} }
}) })
return await umzug.up() return umzug.up()
}, },
async initialize () { async initialize () {
if (config.status === 'READY') { if (config.status === 'READY') {

View File

@@ -12,48 +12,33 @@ try {
const DiskStorage = { const DiskStorage = {
_handleFile (req, file, cb) { _handleFile (req, file, cb) {
const filename = crypto.randomBytes(16).toString('hex') + '.jpg' const filename = crypto.randomBytes(16).toString('hex')
const finalPath = path.resolve(config.upload_path, filename) const sharpStream = sharp({ failOnError: true })
const thumbPath = path.resolve(config.upload_path, 'thumb', filename) const promises = [
const outStream = fs.createWriteStream(finalPath) sharpStream.clone().resize(500, null, { withoutEnlargement: true }).jpeg({ mozjpeg: true, progressive: true }).toFile(path.resolve(config.upload_path, 'thumb', filename + '.jpg')),
const thumbStream = fs.createWriteStream(thumbPath) sharpStream.clone().resize(1200, null, { withoutEnlargement: true } ).jpeg({ quality: 95, mozjpeg: true, progressive: true }).toFile(path.resolve(config.upload_path, filename + '.jpg')),
]
const resizer = sharp().resize(1200).jpeg({ quality: 98 }) file.stream.pipe(sharpStream)
const thumbnailer = sharp().resize(500).jpeg({ quality: 98 }) Promise.all(promises)
let onError = false .then(res => {
const err = e => { const info = res[1]
if (onError) {
log.error('[UPLOAD]', err)
return
}
onError = true
log.error('[UPLOAD]', e)
req.err = e
cb(null)
}
file.stream
.pipe(thumbnailer)
.on('error', err)
.pipe(thumbStream)
.on('error', err)
file.stream
.pipe(resizer)
.on('error', err)
.pipe(outStream)
.on('error', err)
outStream.on('finish', () => {
cb(null, { cb(null, {
destination: config.upload_path, destination: config.upload_path,
filename, filename: filename + '.jpg',
path: finalPath, path: path.resolve(config.upload_path, filename + '.jpg'),
size: outStream.bytesWritten height: info.height,
width: info.width,
size: info.size,
}) })
}) })
.catch(err => {
console.error(err)
req.err = err
cb(null)
})
}, },
_removeFile (req, file, cb) { _removeFile (_req, file, cb) {
delete file.destination delete file.destination
delete file.filename delete file.filename
fs.unlink(file.path, cb) fs.unlink(file.path, cb)

View File

@@ -22,16 +22,17 @@ require('yargs')
.option('config', { .option('config', {
alias: 'c', alias: 'c',
describe: 'Configuration file', describe: 'Configuration file',
default: path.resolve(process.env.cwd, 'config.json') default: path.resolve(process.env.cwd, 'config.json'),
}) coerce: config_path => {
.coerce('config', config_path => {
const absolute_config_path = path.resolve(process.env.cwd, config_path) const absolute_config_path = path.resolve(process.env.cwd, config_path)
process.env.config_path = absolute_config_path process.env.config_path = absolute_config_path
return absolute_config_path return absolute_config_path
}) }})
.command(['accounts'], 'Manage accounts', accountsCLI)
.command(['start', 'run', '$0'], 'Start gancio', {}, start) .command(['start', 'run', '$0'], 'Start gancio', {}, start)
.command(['accounts'], 'Manage accounts', accountsCLI)
.help('h') .help('h')
.alias('h', 'help') .alias('h', 'help')
.epilog('Made with ❤ by underscore hacklab - https://gancio.org') .epilog('Made with ❤ by underscore hacklab - https://gancio.org')
.recommendCommands()
.demandCommand(1, '')
.argv .argv

View File

@@ -1,8 +1,9 @@
let db
function _initializeDB () { function _initializeDB () {
const config = require('../config') const config = require('../config')
config.load() config.load()
config.log_level = 'error' config.log_level = 'error'
const db = require('../api/models/index') db = require('../api/models/index')
return db.initialize() return db.initialize()
} }
@@ -25,7 +26,30 @@ async function modify (args) {
} }
} }
async function add (args) { async function create (args) {
await _initializeDB()
const User = require('../api/models/user')
console.error(args)
const user = await User.create({
email: args.email,
is_active: true,
is_admin: args.admin || false
})
console.error(user)
await db.close()
}
async function remove (args) {
await _initializeDB()
const User = require('../api/models/user')
const user = await User.findOne({
where: { email: args.email }
})
if (user) {
await user.destroy()
}
await db.close()
} }
async function list () { async function list () {
@@ -33,22 +57,31 @@ async function list () {
const User = require('../api/models/user') const User = require('../api/models/user')
const users = await User.findAll() const users = await User.findAll()
console.log() console.log()
users.forEach(u => console.log(`${u.id}\tadmin: ${u.is_admin}\tenabled: ${u.is_active}\temail: ${u.email} - ${u.password}`)) users.forEach(u => console.log(`${u.id}\tadmin: ${u.is_admin}\tenabled: ${u.is_active}\temail: ${u.email}`))
console.log() console.log()
await db.close()
} }
const accountsCLI = yargs => { const accountsCLI = yargs => yargs
return yargs
.command('list', 'List all accounts', list) .command('list', 'List all accounts', list)
.command('modify', 'Modify', { .command('modify', 'Modify', {
account: { account: {
describe: 'Account to modify' describe: 'Account to modify',
type: 'string',
demandOption: true
}, },
'reset-password': { 'reset-password': {
describe: 'Resets the password of the given accoun ' describe: 'Resets the password of the given account ',
type: 'boolean'
} }
}, modify) }, modify)
.command('add', 'Add an account', {}, add) .command('create <email|username>', 'Create an account', {
} admin: { describe: 'Define this account as administrator', type: 'boolean' }
}, create)
.positional('email', { describe: '', type: 'string', demandOption: true })
.command('remove <email|username>', 'Remove an account', {}, remove)
.recommendCommands()
.demandCommand(1, '')
.argv
module.exports = accountsCLI module.exports = accountsCLI

View File

@@ -7,8 +7,8 @@ let config = {
baseurl: '', baseurl: '',
hostname: '', hostname: '',
server: { server: {
host: '0.0.0.0', host: process.env.GANCIO_HOST || '0.0.0.0',
port: 13120 port: process.env.GANCIO_PORT || 13120
}, },
log_level: 'debug', log_level: 'debug',
log_path: path.resolve(process.env.cwd || '', 'logs'), log_path: path.resolve(process.env.cwd || '', 'logs'),

View File

@@ -1,3 +1,4 @@
// needed by sequelize // needed by sequelize
const config = require('./config') const config = require('./config')
config.load()
module.exports = config.db module.exports = config.db

View File

@@ -20,7 +20,7 @@ const log = require('../log')
router.use(cors()) router.use(cors())
// is federation enabled? middleware // is federation enabled? middleware
router.use((req, res, next) => { router.use((_req, res, next) => {
if (settingsController.settings.enable_federation) { return next() } if (settingsController.settings.enable_federation) { return next() }
log.debug('Federation disabled!') log.debug('Federation disabled!')
return res.status(401).send('Federation disabled') return res.status(401).send('Federation disabled')
@@ -29,14 +29,20 @@ router.use((req, res, next) => {
router.use(express.json({ type: ['application/json', 'application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'] })) router.use(express.json({ type: ['application/json', 'application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'] }))
router.get('/m/:event_id', async (req, res) => { router.get('/m/:event_id', async (req, res) => {
const settingsController = require('../api/controller/settings')
log.debug('[AP] Get event details ') log.debug('[AP] Get event details ')
const event_id = req.params.event_id const event_id = req.params.event_id
if (req.accepts('html')) { return res.redirect(301, `/event/${event_id}`) } const acceptHtml = req.accepts('html', 'application/activity+json') === 'html'
if (acceptHtml) { return res.redirect(301, `/event/${event_id}`) }
const event = await Event.findByPk(req.params.event_id, { include: [User, Tag, Place] }) const event = await Event.findByPk(req.params.event_id, { include: [User, Tag, Place] })
if (!event) { return res.status(404).send('Not found') } if (!event) { return res.status(404).send('Not found') }
return res.json(event.toAP(settingsController.settings.instance_name, settingsController.settings.instance_locale)) const eventAp = event.toAP(settingsController.settings.instance_name, settingsController.settings.instance_locale)
eventAp['@context'] = [
"https://www.w3.org/ns/activitystreams"
]
res.type('application/activity+json; charset=utf-8')
return res.json(eventAp)
}) })
// get any message coming from federation // get any message coming from federation

View File

@@ -5,6 +5,7 @@ const APUser = require('../api/models/ap_user')
const log = require('../log') const log = require('../log')
const helpers = require('../helpers') const helpers = require('../helpers')
const linkifyHtml = require('linkify-html') const linkifyHtml = require('linkify-html')
const get = require('lodash/get')
module.exports = { module.exports = {
@@ -59,7 +60,7 @@ module.exports = {
async remove (req, res) { async remove (req, res) {
const resource = await Resource.findOne({ const resource = await Resource.findOne({
where: { activitypub_id: req.body.object.id }, where: { activitypub_id: get(req.body, 'object.id', req.body.object) },
include: [{ model: APUser, required: true, attributes: ['ap_id'] }] include: [{ model: APUser, required: true, attributes: ['ap_id'] }]
}) })
if (!resource) { if (!resource) {

View File

@@ -28,7 +28,7 @@ router.get('/webfinger', allowFederation, (req, res) => {
} }
const resource = req.query.resource const resource = req.query.resource
const domain = (new url.URL(settings.baseurl)).host const domain = (new url.URL(res.locals.settings.baseurl)).host
const [, name, req_domain] = resource.match(/acct:(.*)@(.*)/) const [, name, req_domain] = resource.match(/acct:(.*)@(.*)/)
if (domain !== req_domain) { if (domain !== req_domain) {
log.warn(`Bad webfinger request, requested domain "${req_domain}" instead of "${domain}"`) log.warn(`Bad webfinger request, requested domain "${req_domain}" instead of "${domain}"`)

View File

@@ -7,7 +7,6 @@ const dayjs = require('dayjs')
const config = require('./config') const config = require('./config')
const log = require('./log') const log = require('./log')
const pkg = require('../package.json') const pkg = require('../package.json')
const fs = require('fs')
const path = require('path') const path = require('path')
const sharp = require('sharp') const sharp = require('sharp')
const axios = require('axios') const axios = require('axios')
@@ -95,8 +94,8 @@ module.exports = {
serveStatic () { serveStatic () {
const router = express.Router() const router = express.Router()
// serve event's images/thumb // serve images/thumb
router.use('/media/', express.static(config.upload_path, { immutable: true, maxAge: '1y' } )) router.use('/media/', express.static(config.upload_path, { immutable: true, maxAge: '1y' } ), (_req, res) => res.sendStatus(404))
router.use('/noimg.svg', express.static('./static/noimg.svg')) router.use('/noimg.svg', express.static('./static/noimg.svg'))
router.use('/logo.png', (req, res, next) => { router.use('/logo.png', (req, res, next) => {
@@ -112,7 +111,7 @@ module.exports = {
return router return router
}, },
logRequest (req, res, next) { logRequest (req, _res, next) {
log.debug(`${req.method} ${req.path}`) log.debug(`${req.method} ${req.path}`)
next() next()
}, },
@@ -122,40 +121,33 @@ module.exports = {
if(!/^https?:\/\//.test(url)) { if(!/^https?:\/\//.test(url)) {
throw Error('Hacking attempt?') throw Error('Hacking attempt?')
} }
const filename = crypto.randomBytes(16).toString('hex') + '.jpg'
const finalPath = path.resolve(config.upload_path, filename)
const thumbPath = path.resolve(config.upload_path, 'thumb', filename)
const outStream = fs.createWriteStream(finalPath)
const thumbStream = fs.createWriteStream(thumbPath)
const resizer = sharp().resize(1200).jpeg({ quality: 95 }) const filename = crypto.randomBytes(16).toString('hex')
const thumbnailer = sharp().resize(400).jpeg({ quality: 90 }) const sharpStream = sharp({ failOnError: true })
const promises = [
sharpStream.clone().resize(500, null, { withoutEnlargement: true }).jpeg({ effort: 6, mozjpeg: true }).toFile(path.resolve(config.upload_path, 'thumb', filename + '.jpg')),
sharpStream.clone().resize(1200, null, { withoutEnlargement: true } ).jpeg({ quality: 95, effort: 6, mozjpeg: true}).toFile(path.resolve(config.upload_path, filename + '.jpg')),
]
const response = await axios({ method: 'GET', url, responseType: 'stream' }) const response = await axios({ method: 'GET', url: encodeURI(url), responseType: 'stream' })
return new Promise((resolve, reject) => { response.data.pipe(sharpStream)
let onError = false return Promise.all(promises)
const err = e => { .then(res => {
if (onError) { const info = res[1]
return return {
destination: config.upload_path,
filename: filename + '.jpg',
path: path.resolve(config.upload_path, filename + '.jpg'),
height: info.height,
width: info.width,
size: info.size,
} }
onError = true })
reject(e) .catch(err => {
} log.error(err)
req.err = err
response.data cb(null)
.pipe(thumbnailer)
.on('error', err)
.pipe(thumbStream)
.on('error', err)
response.data
.pipe(resizer)
.on('error', err)
.pipe(outStream)
.on('error', err)
outStream.on('finish', () => resolve(filename))
}) })
}, },
@@ -232,7 +224,8 @@ module.exports = {
}, },
async APRedirect (req, res, next) { async APRedirect (req, res, next) {
if (!req.accepts('html')) { const acceptJson = req.accepts('html', 'application/activity+json') === 'application/activity+json'
if (acceptJson) {
const eventController = require('../server/api/controller/event') const eventController = require('../server/api/controller/event')
const event = await eventController._get(req.params.slug) const event = await eventController._get(req.params.slug)
if (event) { if (event) {

View File

@@ -1,14 +1,29 @@
module.exports = function () {
const config = require('../server/config') const config = require('../server/config')
config.load() config.load()
const initialize = {
// close connections/port/unix socket
async shutdown (exit = true) {
const log = require('../server/log')
const TaskManager = require('../server/taskManager').TaskManager
if (TaskManager) { TaskManager.stop() }
log.info('Closing DB')
const sequelize = require('../server/api/models')
await sequelize.close()
process.off('SIGTERM', initialize.shutdown)
process.off('SIGINT', initialize.shutdown)
if (exit) {
process.exit()
}
},
async start () {
const log = require('../server/log') const log = require('../server/log')
const settingsController = require('./api/controller/settings') const settingsController = require('./api/controller/settings')
const db = require('./api/models/index') const db = require('./api/models/index')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
async function start (nuxt) { dayjs.extend(timezone)
if (config.status == 'READY') { if (config.status == 'READY') {
await db.initialize() await db.initialize()
} else { } else {
@@ -18,18 +33,21 @@ module.exports = function () {
dialect: process.env.GANCIO_DB_DIALECT, dialect: process.env.GANCIO_DB_DIALECT,
storage: process.env.GANCIO_DB_STORAGE, storage: process.env.GANCIO_DB_STORAGE,
host: process.env.GANCIO_DB_HOST, host: process.env.GANCIO_DB_HOST,
port: process.env.GANCIO_DB_PORT,
database: process.env.GANCIO_DB_DATABASE, database: process.env.GANCIO_DB_DATABASE,
username: process.env.GANCIO_DB_USERNAME, username: process.env.GANCIO_DB_USERNAME,
password: process.env.GANCIO_DB_PASSWORD, password: process.env.GANCIO_DB_PASSWORD,
} }
setupController._setupDb(dbConf) setupController._setupDb(dbConf)
.catch(e => { process.exit(1) }) .catch(e => {
log.warn(String(e))
process.exit(1)
})
} }
await settingsController.load() await settingsController.load()
} }
dayjs.extend(timezone)
dayjs.tz.setDefault(settingsController.settings.instance_timezone) dayjs.tz.setDefault(settingsController.settings.instance_timezone)
let TaskManager let TaskManager
@@ -37,22 +55,11 @@ module.exports = function () {
TaskManager = require('../server/taskManager').TaskManager TaskManager = require('../server/taskManager').TaskManager
TaskManager.start() TaskManager.start()
} }
log.info(`Listen on ${config.server.host}:${config.server.port}`)
// close connections/port/unix socket process.on('SIGTERM', initialize.shutdown)
async function shutdown () { process.on('SIGINT', initialize.shutdown)
if (TaskManager) { TaskManager.stop() }
log.info('Closing DB')
const sequelize = require('../server/api/models')
await sequelize.close()
process.off('SIGTERM', shutdown)
process.off('SIGINT', shutdown)
nuxt.close()
process.exit()
} }
process.on('SIGTERM', shutdown)
process.on('SIGINT', shutdown)
} }
return start(this.nuxt)
} module.exports = initialize

View File

@@ -0,0 +1,30 @@
'use strict';
module.exports = {
up (queryInterface, Sequelize) {
return queryInterface.createTable('cohorts', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
allowNull: false,
autoIncrement: true,
},
name: {
type: Sequelize.STRING,
unique: true,
index: true,
allowNull: false
},
isActor: {
type: Sequelize.BOOLEAN
},
isTop: {
type: Sequelize.BOOLEAN
}
})
},
down (queryInterface, Sequelize) {
return queryInterface.dropTable('cohorts')
}
};

View File

@@ -0,0 +1,35 @@
'use strict';
module.exports = {
up (queryInterface, Sequelize) {
return queryInterface.createTable('filters', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
},
cohortId: {
type: Sequelize.INTEGER,
allowNull: true,
references: {
model: 'cohorts',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL'
},
tags: {
type: Sequelize.JSON,
},
places: {
type: Sequelize.JSON,
}
})
},
down (queryInterface, _Sequelize) {
return queryInterface.dropTable('filters')
}
}

View File

@@ -1,6 +1,9 @@
const express = require('express') const express = require('express')
const cookieParser = require('cookie-parser') const cookieParser = require('cookie-parser')
const initialize = require('./initialize.server')
initialize.start()
// const metricsController = require('./metrics') // const metricsController = require('./metrics')
// const promBundle = require('express-prom-bundle') // const promBundle = require('express-prom-bundle')
// const metricsMiddleware = promBundle({ includeMethod: true }) // const metricsMiddleware = promBundle({ includeMethod: true })
@@ -34,11 +37,12 @@ if (config.status === 'READY') {
// rss/ics/atom feed // rss/ics/atom feed
app.get('/feed/:type', cors(), exportController.export) app.get('/feed/:type', cors(), exportController.export)
app.use('/.well-known', webfinger)
app.use('/event/:slug', helpers.APRedirect) app.use('/event/:slug', helpers.APRedirect)
// federation api / activitypub / webfinger / nodeinfo // federation api / activitypub / webfinger / nodeinfo
app.use('/federation', federation) app.use('/federation', federation)
app.use('/.well-known', webfinger)
// ignore unimplemented ping url from fediverse // ignore unimplemented ping url from fediverse
app.use(spamFilter) app.use(spamFilter)
@@ -54,26 +58,34 @@ if (config.status === 'READY') {
app.use('/api', api) app.use('/api', api)
// // Handle 500 // // Handle 500
app.use((error, req, res, next) => { app.use((error, _req, res, _next) => {
log.error('[ERROR]', error) log.error('[ERROR]', error)
res.status(500).send('500: Internal Server Error') return res.status(500).send('500: Internal Server Error')
}) })
// remaining request goes to nuxt // remaining request goes to nuxt
// first nuxt component is ./pages/index.vue (with ./layouts/default.vue) // first nuxt component is ./pages/index.vue (with ./layouts/default.vue)
// prefill current events, tags, places and announcements (used in every path) // prefill current events, tags, places and announcements (used in every path)
app.use(async (req, res, next) => { app.use(async (req, res, next) => {
// const start_datetime = getUnixTime(startOfWeek(startOfMonth(new Date())))
// req.events = await eventController._select(start_datetime, 100)
if (config.status === 'READY') { if (config.status === 'READY') {
const eventController = require('./api/controller/event')
const announceController = require('./api/controller/announce') const announceController = require('./api/controller/announce')
res.locals.meta = await eventController._getMeta()
res.locals.announcements = await announceController._getVisible() res.locals.announcements = await announceController._getVisible()
} }
res.locals.status = config.status res.locals.status = config.status
next() next()
}) })
module.exports = app module.exports = {
handler: app,
load () {
console.error('dentro load !')
},
unload: () => initialize.shutdown(false)
// async unload () {
// const db = require('./api/models/index')
// await db.close()
// process.off('SIGTERM')
// process.off('SIGINT')
// }
}

View File

@@ -4,7 +4,7 @@ function run(fn) {
return fn(); return fn();
} }
function blank_object() { function blank_object() {
return Object.create(null); return /* @__PURE__ */ Object.create(null);
} }
function run_all(fns) { function run_all(fns) {
fns.forEach(run); fns.forEach(run);
@@ -104,7 +104,7 @@ function schedule_update() {
function add_render_callback(fn) { function add_render_callback(fn) {
render_callbacks.push(fn); render_callbacks.push(fn);
} }
const seen_callbacks = new Set(); const seen_callbacks = /* @__PURE__ */ new Set();
let flushidx = 0; let flushidx = 0;
function flush() { function flush() {
const saved_component = current_component; const saved_component = current_component;
@@ -146,7 +146,7 @@ function update($$) {
$$.after_update.forEach(add_render_callback); $$.after_update.forEach(add_render_callback);
} }
} }
const outroing = new Set(); const outroing = /* @__PURE__ */ new Set();
function transition_in(block, local) { function transition_in(block, local) {
if (block && block.i) { if (block && block.i) {
outroing.delete(block); outroing.delete(block);
@@ -282,19 +282,41 @@ if (typeof HTMLElement === "function") {
} }
function get_each_context(ctx, list, i) { function get_each_context(ctx, list, i) {
const child_ctx = ctx.slice(); const child_ctx = ctx.slice();
child_ctx[11] = list[i]; child_ctx[12] = list[i];
return child_ctx; return child_ctx;
} }
function get_each_context_1(ctx, list, i) { function get_each_context_1(ctx, list, i) {
const child_ctx = ctx.slice(); const child_ctx = ctx.slice();
child_ctx[14] = list[i]; child_ctx[15] = list[i];
return child_ctx; return child_ctx;
} }
function create_if_block_5(ctx) {
let link;
return {
c() {
link = element("link");
attr(link, "rel", "stylesheet");
attr(link, "href", ctx[4]);
},
m(target, anchor) {
insert(target, link, anchor);
},
p(ctx2, dirty) {
if (dirty & 16) {
attr(link, "href", ctx2[4]);
}
},
d(detaching) {
if (detaching)
detach(link);
}
};
}
function create_if_block$1(ctx) { function create_if_block$1(ctx) {
let div; let div;
let t; let t;
let if_block = ctx[1] && ctx[3] === "true" && create_if_block_4(ctx); let if_block = ctx[1] && ctx[3] === "true" && create_if_block_4(ctx);
let each_value = ctx[4]; let each_value = ctx[5];
let each_blocks = []; let each_blocks = [];
for (let i = 0; i < each_value.length; i += 1) { for (let i = 0; i < each_value.length; i += 1) {
each_blocks[i] = create_each_block(get_each_context(ctx, each_value, i)); each_blocks[i] = create_each_block(get_each_context(ctx, each_value, i));
@@ -336,8 +358,8 @@ function create_if_block$1(ctx) {
if_block.d(1); if_block.d(1);
if_block = null; if_block = null;
} }
if (dirty & 25) { if (dirty & 41) {
each_value = ctx2[4]; each_value = ctx2[5];
let i; let i;
for (i = 0; i < each_value.length; i += 1) { for (i = 0; i < each_value.length; i += 1) {
const child_ctx = get_each_context(ctx2, each_value, i); const child_ctx = get_each_context(ctx2, each_value, i);
@@ -395,7 +417,7 @@ function create_if_block_4(ctx) {
attr(div0, "class", "title"); attr(div0, "class", "title");
attr(img, "id", "logo"); attr(img, "id", "logo");
attr(img, "alt", "logo"); attr(img, "alt", "logo");
if (!src_url_equal(img.src, img_src_value = "" + (ctx[0] + "/logo.png"))) if (!src_url_equal(img.src, img_src_value = ctx[0] + "/logo.png"))
attr(img, "src", img_src_value); attr(img, "src", img_src_value);
attr(div1, "class", "content"); attr(div1, "class", "content");
attr(a, "href", ctx[0]); attr(a, "href", ctx[0]);
@@ -413,7 +435,7 @@ function create_if_block_4(ctx) {
p(ctx2, dirty) { p(ctx2, dirty) {
if (dirty & 2) if (dirty & 2)
set_data(t0, ctx2[1]); set_data(t0, ctx2[1]);
if (dirty & 1 && !src_url_equal(img.src, img_src_value = "" + (ctx2[0] + "/logo.png"))) { if (dirty & 1 && !src_url_equal(img.src, img_src_value = ctx2[0] + "/logo.png")) {
attr(img, "src", img_src_value); attr(img, "src", img_src_value);
} }
if (dirty & 1) { if (dirty & 1) {
@@ -429,7 +451,7 @@ function create_if_block_4(ctx) {
function create_if_block_2(ctx) { function create_if_block_2(ctx) {
let div; let div;
function select_block_type(ctx2, dirty) { function select_block_type(ctx2, dirty) {
if (ctx2[11].media.length) if (ctx2[12].media.length)
return create_if_block_3; return create_if_block_3;
return create_else_block; return create_else_block;
} }
@@ -472,7 +494,7 @@ function create_else_block(ctx) {
c() { c() {
img = element("img"); img = element("img");
attr(img, "style", "aspect-ratio=1.7778;"); attr(img, "style", "aspect-ratio=1.7778;");
attr(img, "alt", img_alt_value = ctx[11].title); attr(img, "alt", img_alt_value = ctx[12].title);
if (!src_url_equal(img.src, img_src_value = ctx[0] + "/noimg.svg")) if (!src_url_equal(img.src, img_src_value = ctx[0] + "/noimg.svg"))
attr(img, "src", img_src_value); attr(img, "src", img_src_value);
attr(img, "loading", "lazy"); attr(img, "loading", "lazy");
@@ -481,7 +503,7 @@ function create_else_block(ctx) {
insert(target, img, anchor); insert(target, img, anchor);
}, },
p(ctx2, dirty) { p(ctx2, dirty) {
if (dirty & 16 && img_alt_value !== (img_alt_value = ctx2[11].title)) { if (dirty & 32 && img_alt_value !== (img_alt_value = ctx2[12].title)) {
attr(img, "alt", img_alt_value); attr(img, "alt", img_alt_value);
} }
if (dirty & 1 && !src_url_equal(img.src, img_src_value = ctx2[0] + "/noimg.svg")) { if (dirty & 1 && !src_url_equal(img.src, img_src_value = ctx2[0] + "/noimg.svg")) {
@@ -502,9 +524,9 @@ function create_if_block_3(ctx) {
return { return {
c() { c() {
img = element("img"); img = element("img");
attr(img, "style", img_style_value = "object-position: " + position$1(ctx[11]) + "; aspect-ratio=1.7778;"); attr(img, "style", img_style_value = "object-position: " + position$1(ctx[12]) + "; aspect-ratio=1.7778;");
attr(img, "alt", img_alt_value = ctx[11].media[0].name); attr(img, "alt", img_alt_value = ctx[12].media[0].name);
if (!src_url_equal(img.src, img_src_value = ctx[0] + "/media/thumb/" + ctx[11].media[0].url)) if (!src_url_equal(img.src, img_src_value = ctx[0] + "/media/thumb/" + ctx[12].media[0].url))
attr(img, "src", img_src_value); attr(img, "src", img_src_value);
attr(img, "loading", "lazy"); attr(img, "loading", "lazy");
}, },
@@ -512,13 +534,13 @@ function create_if_block_3(ctx) {
insert(target, img, anchor); insert(target, img, anchor);
}, },
p(ctx2, dirty) { p(ctx2, dirty) {
if (dirty & 16 && img_style_value !== (img_style_value = "object-position: " + position$1(ctx2[11]) + "; aspect-ratio=1.7778;")) { if (dirty & 32 && img_style_value !== (img_style_value = "object-position: " + position$1(ctx2[12]) + "; aspect-ratio=1.7778;")) {
attr(img, "style", img_style_value); attr(img, "style", img_style_value);
} }
if (dirty & 16 && img_alt_value !== (img_alt_value = ctx2[11].media[0].name)) { if (dirty & 32 && img_alt_value !== (img_alt_value = ctx2[12].media[0].name)) {
attr(img, "alt", img_alt_value); attr(img, "alt", img_alt_value);
} }
if (dirty & 17 && !src_url_equal(img.src, img_src_value = ctx2[0] + "/media/thumb/" + ctx2[11].media[0].url)) { if (dirty & 33 && !src_url_equal(img.src, img_src_value = ctx2[0] + "/media/thumb/" + ctx2[12].media[0].url)) {
attr(img, "src", img_src_value); attr(img, "src", img_src_value);
} }
}, },
@@ -530,7 +552,7 @@ function create_if_block_3(ctx) {
} }
function create_if_block_1$1(ctx) { function create_if_block_1$1(ctx) {
let div; let div;
let each_value_1 = ctx[11].tags; let each_value_1 = ctx[12].tags;
let each_blocks = []; let each_blocks = [];
for (let i = 0; i < each_value_1.length; i += 1) { for (let i = 0; i < each_value_1.length; i += 1) {
each_blocks[i] = create_each_block_1(get_each_context_1(ctx, each_value_1, i)); each_blocks[i] = create_each_block_1(get_each_context_1(ctx, each_value_1, i));
@@ -550,8 +572,8 @@ function create_if_block_1$1(ctx) {
} }
}, },
p(ctx2, dirty) { p(ctx2, dirty) {
if (dirty & 16) { if (dirty & 32) {
each_value_1 = ctx2[11].tags; each_value_1 = ctx2[12].tags;
let i; let i;
for (i = 0; i < each_value_1.length; i += 1) { for (i = 0; i < each_value_1.length; i += 1) {
const child_ctx = get_each_context_1(ctx2, each_value_1, i); const child_ctx = get_each_context_1(ctx2, each_value_1, i);
@@ -579,7 +601,7 @@ function create_if_block_1$1(ctx) {
function create_each_block_1(ctx) { function create_each_block_1(ctx) {
let span; let span;
let t0; let t0;
let t1_value = ctx[14] + ""; let t1_value = ctx[15] + "";
let t1; let t1;
return { return {
c() { c() {
@@ -594,7 +616,7 @@ function create_each_block_1(ctx) {
append(span, t1); append(span, t1);
}, },
p(ctx2, dirty) { p(ctx2, dirty) {
if (dirty & 16 && t1_value !== (t1_value = ctx2[14] + "")) if (dirty & 32 && t1_value !== (t1_value = ctx2[15] + ""))
set_data(t1, t1_value); set_data(t1, t1_value);
}, },
d(detaching) { d(detaching) {
@@ -608,27 +630,27 @@ function create_each_block(ctx) {
let t0; let t0;
let div2; let div2;
let div0; let div0;
let t1_value = when$1(ctx[11].start_datetime) + ""; let t1_value = when$1(ctx[12].start_datetime) + "";
let t1; let t1;
let t2; let t2;
let div1; let div1;
let t3_value = ctx[11].title + ""; let t3_value = ctx[12].title + "";
let t3; let t3;
let t4; let t4;
let span1; let span1;
let t5; let t5;
let t6_value = ctx[11].place.name + ""; let t6_value = ctx[12].place.name + "";
let t6; let t6;
let t7; let t7;
let span0; let span0;
let t8_value = ctx[11].place.address + ""; let t8_value = ctx[12].place.address + "";
let t8; let t8;
let t9; let t9;
let t10; let t10;
let a_href_value; let a_href_value;
let a_title_value; let a_title_value;
let if_block0 = ctx[3] !== "true" && create_if_block_2(ctx); let if_block0 = ctx[3] !== "true" && create_if_block_2(ctx);
let if_block1 = ctx[11].tags.length && create_if_block_1$1(ctx); let if_block1 = ctx[12].tags.length && create_if_block_1$1(ctx);
return { return {
c() { c() {
a = element("a"); a = element("a");
@@ -657,9 +679,9 @@ function create_each_block(ctx) {
attr(span0, "class", "subtitle"); attr(span0, "class", "subtitle");
attr(span1, "class", "place"); attr(span1, "class", "place");
attr(div2, "class", "content"); attr(div2, "class", "content");
attr(a, "href", a_href_value = "" + (ctx[0] + "/event/" + (ctx[11].slug || ctx[11].id))); attr(a, "href", a_href_value = ctx[0] + "/event/" + (ctx[12].slug || ctx[12].id));
attr(a, "class", "event"); attr(a, "class", "event");
attr(a, "title", a_title_value = ctx[11].title); attr(a, "title", a_title_value = ctx[12].title);
attr(a, "target", "_blank"); attr(a, "target", "_blank");
}, },
m(target, anchor) { m(target, anchor) {
@@ -698,15 +720,15 @@ function create_each_block(ctx) {
if_block0.d(1); if_block0.d(1);
if_block0 = null; if_block0 = null;
} }
if (dirty & 16 && t1_value !== (t1_value = when$1(ctx2[11].start_datetime) + "")) if (dirty & 32 && t1_value !== (t1_value = when$1(ctx2[12].start_datetime) + ""))
set_data(t1, t1_value); set_data(t1, t1_value);
if (dirty & 16 && t3_value !== (t3_value = ctx2[11].title + "")) if (dirty & 32 && t3_value !== (t3_value = ctx2[12].title + ""))
set_data(t3, t3_value); set_data(t3, t3_value);
if (dirty & 16 && t6_value !== (t6_value = ctx2[11].place.name + "")) if (dirty & 32 && t6_value !== (t6_value = ctx2[12].place.name + ""))
set_data(t6, t6_value); set_data(t6, t6_value);
if (dirty & 16 && t8_value !== (t8_value = ctx2[11].place.address + "")) if (dirty & 32 && t8_value !== (t8_value = ctx2[12].place.address + ""))
set_data(t8, t8_value); set_data(t8, t8_value);
if (ctx2[11].tags.length) { if (ctx2[12].tags.length) {
if (if_block1) { if (if_block1) {
if_block1.p(ctx2, dirty); if_block1.p(ctx2, dirty);
} else { } else {
@@ -718,10 +740,10 @@ function create_each_block(ctx) {
if_block1.d(1); if_block1.d(1);
if_block1 = null; if_block1 = null;
} }
if (dirty & 17 && a_href_value !== (a_href_value = "" + (ctx2[0] + "/event/" + (ctx2[11].slug || ctx2[11].id)))) { if (dirty & 33 && a_href_value !== (a_href_value = ctx2[0] + "/event/" + (ctx2[12].slug || ctx2[12].id))) {
attr(a, "href", a_href_value); attr(a, "href", a_href_value);
} }
if (dirty & 16 && a_title_value !== (a_title_value = ctx2[11].title)) { if (dirty & 32 && a_title_value !== (a_title_value = ctx2[12].title)) {
attr(a, "title", a_title_value); attr(a, "title", a_title_value);
} }
}, },
@@ -736,41 +758,65 @@ function create_each_block(ctx) {
}; };
} }
function create_fragment$1(ctx) { function create_fragment$1(ctx) {
let if_block_anchor; let t;
let if_block = ctx[4].length && create_if_block$1(ctx); let if_block1_anchor;
let if_block0 = ctx[4] && create_if_block_5(ctx);
let if_block1 = ctx[5].length && create_if_block$1(ctx);
return { return {
c() { c() {
if (if_block) if (if_block0)
if_block.c(); if_block0.c();
if_block_anchor = empty(); t = space();
if (if_block1)
if_block1.c();
if_block1_anchor = empty();
this.c = noop; this.c = noop;
}, },
m(target, anchor) { m(target, anchor) {
if (if_block) if (if_block0)
if_block.m(target, anchor); if_block0.m(target, anchor);
insert(target, if_block_anchor, anchor); insert(target, t, anchor);
if (if_block1)
if_block1.m(target, anchor);
insert(target, if_block1_anchor, anchor);
}, },
p(ctx2, [dirty]) { p(ctx2, [dirty]) {
if (ctx2[4].length) { if (ctx2[4]) {
if (if_block) { if (if_block0) {
if_block.p(ctx2, dirty); if_block0.p(ctx2, dirty);
} else { } else {
if_block = create_if_block$1(ctx2); if_block0 = create_if_block_5(ctx2);
if_block.c(); if_block0.c();
if_block.m(if_block_anchor.parentNode, if_block_anchor); if_block0.m(t.parentNode, t);
} }
} else if (if_block) { } else if (if_block0) {
if_block.d(1); if_block0.d(1);
if_block = null; if_block0 = null;
}
if (ctx2[5].length) {
if (if_block1) {
if_block1.p(ctx2, dirty);
} else {
if_block1 = create_if_block$1(ctx2);
if_block1.c();
if_block1.m(if_block1_anchor.parentNode, if_block1_anchor);
}
} else if (if_block1) {
if_block1.d(1);
if_block1 = null;
} }
}, },
i: noop, i: noop,
o: noop, o: noop,
d(detaching) { d(detaching) {
if (if_block) if (if_block0)
if_block.d(detaching); if_block0.d(detaching);
if (detaching) if (detaching)
detach(if_block_anchor); detach(t);
if (if_block1)
if_block1.d(detaching);
if (detaching)
detach(if_block1_anchor);
} }
}; };
} }
@@ -799,6 +845,7 @@ function instance$1($$self, $$props, $$invalidate) {
let { theme = "light" } = $$props; let { theme = "light" } = $$props;
let { show_recurrent = false } = $$props; let { show_recurrent = false } = $$props;
let { sidebar = "true" } = $$props; let { sidebar = "true" } = $$props;
let { external_style = "" } = $$props;
let mounted = false; let mounted = false;
let events = []; let events = [];
function update2(v) { function update2(v) {
@@ -814,11 +861,9 @@ function instance$1($$self, $$props, $$invalidate) {
if (places) { if (places) {
params.push(`places=${places}`); params.push(`places=${places}`);
} }
if (show_recurrent) { params.push(`show_recurrent=${show_recurrent ? "true" : "false"}`);
params.push(`show_recurrent=true`);
}
fetch(`${baseurl}/api/events?${params.join("&")}`).then((res) => res.json()).then((e) => { fetch(`${baseurl}/api/events?${params.join("&")}`).then((res) => res.json()).then((e) => {
$$invalidate(4, events = e); $$invalidate(5, events = e);
}).catch((e) => { }).catch((e) => {
console.error("Error loading Gancio API -> ", e); console.error("Error loading Gancio API -> ", e);
}); });
@@ -833,20 +878,22 @@ function instance$1($$self, $$props, $$invalidate) {
if ("title" in $$props2) if ("title" in $$props2)
$$invalidate(1, title = $$props2.title); $$invalidate(1, title = $$props2.title);
if ("maxlength" in $$props2) if ("maxlength" in $$props2)
$$invalidate(5, maxlength = $$props2.maxlength); $$invalidate(6, maxlength = $$props2.maxlength);
if ("tags" in $$props2) if ("tags" in $$props2)
$$invalidate(6, tags = $$props2.tags); $$invalidate(7, tags = $$props2.tags);
if ("places" in $$props2) if ("places" in $$props2)
$$invalidate(7, places = $$props2.places); $$invalidate(8, places = $$props2.places);
if ("theme" in $$props2) if ("theme" in $$props2)
$$invalidate(2, theme = $$props2.theme); $$invalidate(2, theme = $$props2.theme);
if ("show_recurrent" in $$props2) if ("show_recurrent" in $$props2)
$$invalidate(8, show_recurrent = $$props2.show_recurrent); $$invalidate(9, show_recurrent = $$props2.show_recurrent);
if ("sidebar" in $$props2) if ("sidebar" in $$props2)
$$invalidate(3, sidebar = $$props2.sidebar); $$invalidate(3, sidebar = $$props2.sidebar);
if ("external_style" in $$props2)
$$invalidate(4, external_style = $$props2.external_style);
}; };
$$self.$$.update = () => { $$self.$$.update = () => {
if ($$self.$$.dirty & 494) { if ($$self.$$.dirty & 974) {
update2(); update2();
} }
}; };
@@ -855,6 +902,7 @@ function instance$1($$self, $$props, $$invalidate) {
title, title,
theme, theme,
sidebar, sidebar,
external_style,
events, events,
maxlength, maxlength,
tags, tags,
@@ -873,12 +921,13 @@ class GancioEvents extends SvelteElement {
}, instance$1, create_fragment$1, safe_not_equal, { }, instance$1, create_fragment$1, safe_not_equal, {
baseurl: 0, baseurl: 0,
title: 1, title: 1,
maxlength: 5, maxlength: 6,
tags: 6, tags: 7,
places: 7, places: 8,
theme: 2, theme: 2,
show_recurrent: 8, show_recurrent: 9,
sidebar: 3 sidebar: 3,
external_style: 4
}, null); }, null);
if (options) { if (options) {
if (options.target) { if (options.target) {
@@ -899,7 +948,8 @@ class GancioEvents extends SvelteElement {
"places", "places",
"theme", "theme",
"show_recurrent", "show_recurrent",
"sidebar" "sidebar",
"external_style"
]; ];
} }
get baseurl() { get baseurl() {
@@ -917,21 +967,21 @@ class GancioEvents extends SvelteElement {
flush(); flush();
} }
get maxlength() { get maxlength() {
return this.$$.ctx[5]; return this.$$.ctx[6];
} }
set maxlength(maxlength) { set maxlength(maxlength) {
this.$$set({ maxlength }); this.$$set({ maxlength });
flush(); flush();
} }
get tags() { get tags() {
return this.$$.ctx[6]; return this.$$.ctx[7];
} }
set tags(tags) { set tags(tags) {
this.$$set({ tags }); this.$$set({ tags });
flush(); flush();
} }
get places() { get places() {
return this.$$.ctx[7]; return this.$$.ctx[8];
} }
set places(places) { set places(places) {
this.$$set({ places }); this.$$set({ places });
@@ -945,7 +995,7 @@ class GancioEvents extends SvelteElement {
flush(); flush();
} }
get show_recurrent() { get show_recurrent() {
return this.$$.ctx[8]; return this.$$.ctx[9];
} }
set show_recurrent(show_recurrent) { set show_recurrent(show_recurrent) {
this.$$set({ show_recurrent }); this.$$set({ show_recurrent });
@@ -958,6 +1008,13 @@ class GancioEvents extends SvelteElement {
this.$$set({ sidebar }); this.$$set({ sidebar });
flush(); flush();
} }
get external_style() {
return this.$$.ctx[4];
}
set external_style(external_style) {
this.$$set({ external_style });
flush();
}
} }
customElements.define("gancio-events", GancioEvents); customElements.define("gancio-events", GancioEvents);
function create_if_block(ctx) { function create_if_block(ctx) {
@@ -996,7 +1053,7 @@ function create_if_block(ctx) {
t6 = text(t6_value); t6 = text(t6_value);
attr(div1, "class", "place"); attr(div1, "class", "place");
attr(div2, "class", "container"); attr(div2, "class", "container");
attr(a, "href", a_href_value = "" + (ctx[0] + "/event/" + (ctx[1].slug || ctx[1].id))); attr(a, "href", a_href_value = ctx[0] + "/event/" + (ctx[1].slug || ctx[1].id));
attr(a, "class", "card"); attr(a, "class", "card");
attr(a, "target", "_blank"); attr(a, "target", "_blank");
}, },
@@ -1035,7 +1092,7 @@ function create_if_block(ctx) {
set_data(t3, t3_value); set_data(t3, t3_value);
if (dirty & 2 && t6_value !== (t6_value = ctx2[1].place.name + "")) if (dirty & 2 && t6_value !== (t6_value = ctx2[1].place.name + ""))
set_data(t6, t6_value); set_data(t6, t6_value);
if (dirty & 3 && a_href_value !== (a_href_value = "" + (ctx2[0] + "/event/" + (ctx2[1].slug || ctx2[1].id)))) { if (dirty & 3 && a_href_value !== (a_href_value = ctx2[0] + "/event/" + (ctx2[1].slug || ctx2[1].id))) {
attr(a, "href", a_href_value); attr(a, "href", a_href_value);
} }
}, },

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