Merge branch 'master' into gh

This commit is contained in:
lesion 2022-06-06 16:57:05 +02:00
commit 616c54229a
No known key found for this signature in database
GPG key ID: 352918250B012177
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)
### Gancio dev configuration
tests/seeds/testdb.sqlite
preso.md
gancio.sqlite
db.sqlite
releases

View file

@ -1,7 +1,37 @@
All notable changes to this project will be documented in this file.
### UNRELEASED
- add CLI support to manage accounts (list / modify / add accounts)
### 1.5.0 - UNRELEASED
- 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
- fix [#140](https://framagit.org/les/gancio/-/issues/140) - Invalid date
- 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'
export function attributesFromEvents (_events, _tags) {
const colors = ['blue', 'orange', 'yellow', 'teal', 'indigo', 'green', 'red', 'purple', 'pink', 'gray']
const tags = take(_tags, 10).map(t => t.tag)
export function attributesFromEvents (_events) {
// const colors = ['teal', 'green', 'yellow', 'teal', 'indigo', 'green', 'red', 'purple', 'pink', 'gray']
// merge events with same date
let attributes = []
attributes.push({ key: 'today', dates: new Date(), bar: { color: 'green', fillMode: 'outline' } })
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 color = { class: 'vc-rounded-full', color: 'blue', fillMode: where === 'base' ? 'light' : 'solid' }
const tag = get(event, 'tags[0]')
if (event.start_datetime < now) {
if (event.multidate) {
color.fillMode = where === 'base' ? 'light' : 'outline'
if (where === 'base') {
color.class += ' vc-past'
const i = attributes.find(a => a.day === key)
if (!i) {
attributes.push({ day: key, key: e.id, n: 1, dates: new Date(e.start_datetime * 1000),
dot: { color: 'teal', class: c } })
continue
}
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 {
color.class += ' vc-past'
}
}
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
i.dot = { color: 'teal', class: c }
}
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
.filter(e => e.multidate)
.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) }
})))
// add a bar to highlight today
attributes.push({ key: 'today', dates: new Date(), highlight: { color: 'green', fillMode: 'outline' } })
return attributes
}

View file

@ -31,7 +31,8 @@ li {
}
#calh {
height: 292px;
/* this is to avoid content shift layout as v-calendar does not support SSR */
height: 268px;
}
.container {
@ -55,7 +56,6 @@ li {
scrollbar-color: #FF4511 #111;
}
// EVENT
.event {
display: flex;
position: relative;
@ -67,8 +67,9 @@ li {
margin-right: .4em;
transition: all .5s;
overflow: hidden;
}
.title {
.event .title {
display: -webkit-box;
overflow: hidden;
margin: 0.5rem 1rem 0.5rem 1rem;
@ -77,33 +78,22 @@ li {
-webkit-box-orient: vertical;
font-size: 1.1em !important;
line-height: 1.2em !important;
text-decoration: none;
}
.body {
.event .body {
flex: 1 1 auto;
}
.img {
width: 100%;
max-height: 250px;
min-height: 160px;
object-fit: cover;
object-position: top;
aspect-ratio: 1.7778;
}
.place {
max-width: 100%;
span {
.event .place span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
a {
.event a {
text-decoration: none;
}
}
.vc-past {
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">
nuxt-link(:to='`/announcement/${announcement.id}`')
v-alert.mb-1(border='left' type='info' color="primary" :icon='mdiInformation') {{announcement.title}}
<template>
<nuxt-link :to='`/announcement/${announcement.id}`'>
<v-alert class='mb-1' outlined type='info' color="primary" :icon='mdiInformation'>{{announcement.title}}</v-alert>
</nuxt-link>
</template>
<script>
import { mdiInformation } from '@mdi/js'

View file

@ -16,7 +16,7 @@
</template>
<script>
import { mapState, mapActions } from 'vuex'
import { mapState } from 'vuex'
import dayjs from 'dayjs'
import { attributesFromEvents } from '../assets/helper'
@ -26,25 +26,22 @@ export default {
events: { type: Array, default: () => [] }
},
data () {
const month = dayjs().month() + 1
const year = dayjs().year()
const month = dayjs.tz().month() + 1
const year = dayjs.tz().year()
return {
selectedDate: null,
page: { month, year }
}
},
computed: {
...mapState(['tags', 'filters', 'in_past', 'settings']),
...mapState(['settings']),
attributes () {
return attributesFromEvents(this.events, this.tags)
return attributesFromEvents(this.events)
}
},
methods: {
...mapActions(['updateEvents', 'showPastEvents']),
updatePage (page) {
return new Promise((resolve, reject) => {
this.$emit('monthchange', page)
})
},
click (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-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'
:prepend-icon="mdiClockTimeFourOutline"
: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-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'
:value='dueHour' clearable
:items='hourList' @change='hr => change("dueHour", hr)')
:prepend-icon="mdiClockTimeEightOutline"
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")')
@ -52,7 +90,8 @@ v-col(cols=12)
import dayjs from 'dayjs'
import { mapState } from 'vuex'
import List from '@/components/List'
import { attributesFromEvents } from '../../assets/helper'
import { attributesFromEvents } from '../assets/helper'
import { mdiClockTimeFourOutline, mdiClockTimeEightOutline } from '@mdi/js'
export default {
name: 'DateInput',
@ -63,6 +102,10 @@ export default {
},
data () {
return {
mdiClockTimeFourOutline, mdiClockTimeEightOutline,
allowedMinutes: [0, 15, 30, 45],
menuFromHour: false,
menuDueHour: false,
type: 'normal',
page: null,
events: [],
@ -74,15 +117,14 @@ export default {
}
},
computed: {
...mapState(['settings', 'tags']),
...mapState(['settings']),
todayEvents () {
const start = dayjs(this.value.from).startOf('day').unix()
const end = dayjs(this.value.from).endOf('day').unix()
const events = this.events.filter(e => e.start_datetime >= start && e.start_datetime <= end)
return events
return this.events.filter(e => e.start_datetime >= start && e.start_datetime <= end)
},
attributes () {
return attributesFromEvents(this.events, this.tags)
return attributesFromEvents(this.events)
},
fromDate () {
if (this.value.multidate) {
@ -92,21 +134,10 @@ export default {
},
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 () {
return this.value.due && this.value.dueHour ? dayjs(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
return this.value.due && this.value.dueHour ? dayjs.tz(this.value.due).format('HH:mm') : null
},
whenPatterns () {
if (!this.value.from) { return }
@ -196,7 +227,7 @@ export default {
} else if (what === 'fromHour') {
if (value) {
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 })
} else {
this.$emit('input', { ...this.value, fromHour: false })
@ -204,7 +235,7 @@ export default {
} else if (what === 'dueHour') {
if (value) {
const [hour, minute] = value.split(':')
const fromHour = dayjs(this.value.from).hour()
const fromHour = dayjs.tz(this.value.from).hour()
// add a day
let due = dayjs(this.value.from)
@ -226,20 +257,20 @@ export default {
let from = value.start
let due = value.end
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) {
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 })
} else {
let from = value
let due = this.value.due
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) {
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 })
}

View file

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

View file

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

View file

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

View file

@ -15,7 +15,7 @@
v-col.col-12.col-sm-4
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 }")
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='error' @click='remove') {{$t('common.remove')}}
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}}
v-file-input(
v-else
@ -53,7 +53,7 @@ export default {
name: 'MediaInput',
props: {
value: { type: Object, default: () => ({ image: null }) },
event: { type: Object, default: () => {} }
event: { type: Object, default: () => ({}) }
},
data () {
return {
@ -142,7 +142,7 @@ export default {
cursor: crosshair;
}
.img {
.mediaPreview {
width: 100%;
object-fit: cover;
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)
//- logo, title and description
v-list-item(:to='$route.name==="index"?"/about":"/"')
v-list-item-avatar(tile)
v-img(src='/logo.png' alt='home')
v-list-item-content.d-none.d-sm-flex
v-list-item-title
v-list-item.pa-0(:to='$route.name==="index"?"/about":"/"')
v-list-item-avatar.ma-xs-1(tile)
img(src='/logo.png' height='40')
v-list-item-content.d-flex
v-list-item-title.d-flex
h2 {{settings.title}}
v-list-item-subtitle {{settings.description}}
v-list-item-subtitle.d-none.d-sm-flex {{settings.description}}
v-spacer
v-btn(v-if='$auth.loggedIn || settings.allow_anon_event' icon nuxt to='/add' :aria-label='$t("common.add_event")' :title='$t("common.add_event")')
@ -21,7 +21,7 @@
v-icon(v-text='mdiLogin')
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 }")
v-btn(icon v-bind='attrs' v-on='on' title='Menu' aria-label='Menu')
v-icon(v-text='mdiDotsVertical')
@ -48,7 +48,7 @@
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')
</template>
@ -64,25 +64,7 @@ export default {
return { mdiPlus, mdiShareVariant, mdiLogout, mdiLogin, mdiDotsVertical, mdiAccount, mdiCog, mdiRss }
},
mixins: [clipboard],
computed: {
...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}`
},
},
computed: mapState(['settings']),
methods: {
logout () {
this.$root.$message('common.logout_ok')

View file

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

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

View file

@ -126,6 +126,7 @@ export default {
if (!this.instance_url.startsWith('http')) {
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`)
this.setSetting({
key: 'trusted_instances',

View file

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

View file

@ -9,7 +9,7 @@
accept='image/*')
template(slot='append-outer')
v-btn(color='warning' text @click='resetLogo') <v-icon v-text='mdiRestore'></v-icon> {{$t('common.reset')}}
v-img(:src='`${settings.baseurl}/logo.png?${logoKey}`'
v-img(:src='`/logo.png?${logoKey}`'
max-width="60px" max-height="60px" contain)
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-card
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)')
v-list-item-content
v-list-item-title {{link.label}}
v-list-item-subtitle {{link.href}}
v-list-item-action
v-btn(icon color='error' @click.stop='removeFooterLink(link)')
v-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')
</template>
<script>
import { mapActions, mapState } from 'vuex'
import { mdiDeleteForever, mdiRestore, mdiPlus } from '@mdi/js'
import { mdiDeleteForever, mdiRestore, mdiPlus, mdiChevronUp } from '@mdi/js'
export default {
name: 'Theme',
data () {
return {
mdiDeleteForever, mdiRestore, mdiPlus,
mdiDeleteForever, mdiRestore, mdiPlus, mdiChevronUp,
valid: false,
logoKey: 0,
link: { href: '', label: '' },
@ -152,6 +154,12 @@ export default {
const footerLinks = this.settings.footerLinks.filter(l => l.label !== item.label)
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) {
this.link = { href: item.href, label: item.label }
this.linkModal = true

View file

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

View file

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

View file

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

View file

@ -1,16 +1,38 @@
---
layout: default
title: Admin
permalink: /admin
nav_order: 5
permalink: /usage/admin
nav_order: 1
parent: Usage
---
# Admin
{: .no_toc }
### CLI
#### 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
## Add user

View file

@ -7,6 +7,12 @@
<meta name="Description" content="{{ page.description }}">
{% 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="stylesheet" href="{{ '/assets/css/just-the-docs-default.css' | absolute_url }}">
<link rel="stylesheet" href="{{ '/assets/css/premonition.css' | absolute_url }}">

View file

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

View file

@ -8,6 +8,24 @@ nav_order: 10
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
- fix [#140](https://framagit.org/les/gancio/-/issues/140) - Invalid date
- 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 cache clean
RUN yarn global add --latest --production --silent https://gancio.org/latest.tgz

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ services:
gancio:
build: .
restart: always
image: node:17.4-slim
image: gancio
container_name: gancio
environment:
- PATH=$PATH:/home/node/.yarn/bin
@ -13,7 +13,7 @@ services:
- GANCIO_DB_DIALECT=sqlite
- GANCIO_DB_STORAGE=./gancio.sqlite
entrypoint: /entrypoint.sh
command: gancio start --docker
command: gancio start
volumes:
- ./data:/home/node/data
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)
- **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
- Multi-day events (festival, conferences...)
- Recurring events (each monday, each two monday, each monday and friday, each two saturday, etc.)
- Filter events for tags or places
- RSS and ICS export (with filters)
- 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!
- 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
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
```

View file

@ -9,9 +9,12 @@ nav_order: 7
- [gancio.cisti.org](https://gancio.cisti.org) (Turin, Italy)
- [lapunta.org](https://lapunta.org) (Florence, Italy)
- [sapratza.in](https://sapratza.in/) (Sardinia, Italy)
- [termine.161.social](https://termine.161.social) (Germany)
- [bcn.convoca.la](https://bcn.convoca.la/) (Barcelona)
- [ezkerraldea.euskaragendak.eus](https://ezkerraldea.euskaragendak.eus/)
- [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>

View file

@ -9,3 +9,5 @@ has_children: true
## Usage
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'>
v-app(app)
Snackbar
Confirm
Nav
<template>
<v-app app>
<Snackbar/>
<Confirm/>
<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-fade-transition(hide-on-leave)
nuxt
</v-app>
Footer
</template>
<script>
@ -17,6 +24,7 @@ import Snackbar from '../components/Snackbar'
import Footer from '../components/Footer'
import Confirm from '../components/Confirm'
import { mapState } from 'vuex'
import { mdiChevronLeft } from '@mdi/js'
export default {
head () {
@ -26,9 +34,24 @@ export default {
}
}
},
data () {
return { cohorts: [], mdiChevronLeft }
},
async fetch () {
this.cohorts = await this.$axios.$get('cohorts')
},
name: 'Default',
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 () {
this.$vuetify.theme.dark = this.settings['theme.is_dark']
}

View file

@ -2,7 +2,7 @@
v-app#iframe
nuxt
</template>
<style lang='less'>
<style>
#iframe.v-application {
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"
},
"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: ",
"complete": "El registre ha de ser confirmat.",
"first_user": "S'ha creat i activat un compte administrador"
@ -124,7 +124,7 @@
"media_description": "Pots adjuntar un cartell (opcional)",
"added": "S'ha afegit l'activitat",
"added_anon": "S'ha afegit l'activitat però encara ha de ser confirmada.",
"where_description": "On es farà? Si no 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",
"not_found": "No s'ha trobat l'activitat",
"remove_confirmation": "Segur que vols esborrar l'activitat?",
@ -150,7 +150,7 @@
"from": "Des de les",
"image_too_big": "La imatge és massa gran! Max 4 MB",
"interact_with_me_at": "Interacciona amb mi a",
"follow_me_description": "Entre les diverses maneres d'estar al dia de les activitats que es publiquen aquí a {title},\n pots seguir-nos al compte <u>{account}</u> des de Mastodon o altres, i afegir recursos des d'allà. <br/> <br/>\nSi no has sentit mai sobre «Mastodon» o «Fedivers», recomanem 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",
"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",
@ -159,7 +159,11 @@
"edit_recurrent": "Edita l'activitat periòdica:",
"updated": "S'ha actualitzat l'activitat",
"saved": "S'ha desat l'activitat",
"import_description": "Pots importar activitats des d'altres instàncies o plataformes que facin servir formats estàndards (ics o h-event)"
"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": {
"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_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",
"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": {
"not_confirmed": "Encara no s'ha confirmat…",
@ -272,6 +278,8 @@
"setup": {
"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>",
"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": {
"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",
"import": "Import",
"max_events": "N. max events",
"label": "Label"
"label": "Label",
"blobs": "Blobs"
},
"login": {
"description": "By logging in you can publish new events.",
@ -159,7 +160,11 @@
"import_URL": "Import from URL",
"import_ICS": "Import from ICS",
"ics": "ICS",
"import_description": "You can import events from other platforms and other instances through standard formats (ics and h-event)"
"import_description": "You can import events from other platforms and other instances through standard formats (ics and h-event)",
"alt_text_description": "Description for people with visual impairments",
"choose_focal_point": "Choose the focal point",
"remove_media_confirmation": "Do you confirm the image removal?",
"download_flyer": "Download flyer"
},
"admin": {
"place_description": "If you have gotten the place or address wrong, you can change it.<br/>All current and past events associated with this place will change address.",
@ -170,6 +175,7 @@
"delete_user_confirm": "Are you sure you want to remove {user}?",
"user_remove_ok": "User removed",
"user_create_ok": "User created",
"event_remove_ok": "Event removed",
"allow_registration_description": "Allow open registrations?",
"allow_anon_event": "Allow anonymous events (has to be confirmed)?",
"allow_recurrent_event": "Allow recurring events",
@ -226,7 +232,8 @@
"smtp_test_success": "A test email is sent to {admin_email}, please check your inbox",
"smtp_test_button": "Send a test email",
"admin_email": "Admin e-mail",
"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": {
"not_confirmed": "Not confirmed yet…",
@ -273,6 +280,7 @@
"completed": "Setup completed",
"completed_description": "<p>You can now login with the following user:<br/><br/>User: <b>{email}</b><br/>Password: <b>{password}<b/></p>",
"copy_password_dialog": "Yes, you have to copy the password!",
"start": "Start"
"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"
},
"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: ",
"complete": "Confirmaremos el registro lo antes posible.",
"first_user": "Administrador creado y activado"
},
"event": {
"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",
"what_description": "Nombre evento",
"description_description": "Descripción, puedes copiar y pegar",
@ -148,7 +148,7 @@
"from": "Desde las",
"image_too_big": "La imagén es demasiado grande! Tamaño máx 4M",
"interact_with_me_at": "Sígueme en el fediverso en",
"show_recurrent": "Eventos recurrientes",
"show_recurrent": "Eventos recurrentes",
"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)",
"interact_with_me": "Sigueme en el fediverso",
@ -221,7 +221,14 @@
"is_dark": "Tema oscuro",
"instance_block_confirm": "¿Estás seguro/a que quieres bloquear la instancia {instance}?",
"add_instance": "Añadir instancia",
"disable_user_confirm": "Estas seguro de que quieres deshabilitar a {user}?"
"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": {
"not_confirmed": "Todavía no hemos confirmado este email…",
@ -263,5 +270,10 @@
"validators": {
"email": "Introduce un correo electrónico valido",
"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"
},
"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: ",
"complete": "Izen-ematea baieztatua izan behar da.",
"first_user": "Administratzailea sortu da"
@ -159,7 +159,11 @@
"only_future": "datozen ekitaldiak bakarrik",
"edit_recurrent": "Editatu ekitaldi errepikaria:",
"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": {
"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",
"admin_email": "Administratzailearen eposta",
"smtp_hostname": "SMTP hostname",
"smtp_description": "<ul><li>Administratzaileak eposta bat jaso beharko luke anonimo batek ekitaldi bat gehitzen duenean (gaituta badago).</li><li>Administratzaileak eposta bat jaso beharko luke izena emateko eskari bakoitzeko (gaituta badago).</li><li>Erabiltzaileak eposta bat jaso beharko luke izena emateko eskariarekin.</li><li>Erabiltzaileak eposta bat jaso beharko luke izen ematea baieztatzean.</li><li>Erabiltzaileak eposta bat jaso beharko luke administratzaileak zuzenean izena emanez gero.</li><li>Erabiltzaileek eposta bat jaso beharko lukete pasahitza ahazten dutenean.</li></ul>"
"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": {
"not_confirmed": "Oraindik baieztatu gabe dago…",
@ -271,6 +278,8 @@
"setup": {
"start": "Hasi",
"completed": "Instalazioa bukatu da",
"completed_description": "<p>Erabiltzaile honekin saioa has dezakezu orain:<br/><br/>Erabiltzailea: <b>{email}</b><br/>Pasahitza: <b>{password}<b/></p>"
"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 :",
"updated": "Évènement mis à jour",
"import_description": "Vous pouvez importer des événements depuis d'autres plateformes ou d'autres instances à travers des formats standards (ics et h-event)",
"saved": "Événement enregistré"
"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": {
"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",
"completed": "Configuration terminée",
"completed_description": "<p>Vous pouvez désormais vous connectez avec le compte utilisateur suivant :<br/><br/>Identifiant : <b>{email}</b><br/>Mot de passe : <b>{password}<b/></p>",
"start": "Commencer"
"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",
"follow_me_description": "Un dos xeitos de recibir actualizacións dos eventos que se publican aquí en {title},\né seguindo a conta <u>{account}</u> no fediverso, por exemplo a través de Mastodon, e posiblemente tamén engadir recursos para un evento desde alí.<br/><br/>\nSe nunco escoitaches falar de Mastodon e o fediverso recomendámosche ler <a href='https://www.savjee.be/videos/simply-explained/mastodon-and-fediverse-explained/'>este artigo</a>.<br/><br/>Escribe aquí a túa instancia (ex. mastodon.social)",
"ics": "ICS",
"import_description": "Podes importar eventos desde outras plataformas e outras instancias usando formatos estándar (ics e h-event)"
"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": {
"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",
"smtp_hostname": "Servidor SMTP",
"new_announcement": "Novo anuncio",
"smtp_description": "<ul><li>Admin debería recibir un email cando se engade un evento anónimo (se está activo)</li><li>Admin debería recibir un email coas solicitudes de rexistro (se activo).</li><li>A usuaria debería recibir un email coa solicitude de rexistro.</li><li>A usuaria debería recibir un email confirmando o rexistro.</li><li>A usuaria debería recibir un email de confirmación cando fose subscrita directamente por Admin</li><li>As usuarias deberían recibir un email para restablecer o contrasinal se o esquecen</li></ul>"
"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": {
"not_confirmed": "Aínda non foi confirmado…",
@ -263,7 +269,9 @@
"setup": {
"completed": "Configuración completada",
"completed_description": "<p>Xa podes acceder con estas credenciais:<br/><br/>Identificador: <b>{email}</b><br/>Contrasinal: <b>{password}</b></p>",
"start": "Comezar"
"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": {
"forgot_password": "Esqueceches o contrasinal?",

View file

@ -86,7 +86,8 @@
"reset": "Reset",
"import": "Importa",
"max_events": "N. massimo eventi",
"label": "Etichetta"
"label": "Etichetta",
"blobs": "Bolle"
},
"login": {
"description": "Entrando puoi pubblicare nuovi eventi.",
@ -151,7 +152,7 @@
"each_month": "Ogni mese",
"due": "alle",
"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",
"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",
@ -160,9 +161,10 @@
"import_URL": "Importa da URL (ics o h-event)",
"ics": "ICS",
"import_description": "Puoi importare eventi da altre piattaforme e da altre istanze attraverso i formati standard (ics e h-event)",
"alt_text_description": "Descrizione per utenti con disabilità visive",
"alt_text_description": "Descrizione per persone con disabilità visive",
"choose_focal_point": "Scegli il punto centrale cliccando",
"remove_media_confirmation": "Confermi l'eliminazione dell'immagine?"
"remove_media_confirmation": "Confermi l'eliminazione dell'immagine?",
"download_flyer": "Scarica volantino"
},
"admin": {
"place_description": "Nel caso in cui un luogo sia errato o cambi indirizzo, puoi modificarlo.<br/>Considera che tutti gli eventi associati a questo luogo cambieranno indirizzo (anche quelli passati).",
@ -276,6 +278,7 @@
"completed": "Setup completato",
"completed_description": "<p>Puoi entrare con le seguenti credenziali:<br/><br/>Utente: <b>{email}</b><br/>Password: <b>{password}<b/></p>",
"copy_password_dialog": "Sì, devi copiare la password!",
"start": "Inizia"
"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' }
],
link: [{ rel: 'icon', type: 'image/png', href: '/logo.png' }],
link: [{ rel: 'preload', type: 'image/png', href: '/logo.png', as: 'image' }],
script: [{ src: '/gancio-events.es.js', async: true, body: true }],
},
dev: isDev,
@ -27,13 +26,12 @@ module.exports = {
}
},
css: ['./assets/style.less'],
css: ['./assets/style.css'],
/*
** Customize the progress-bar component
*/
loading: '~/components/Loading.vue',
/*
** Plugins to load before mounting the App
*/
@ -53,9 +51,27 @@ module.exports = {
// Doc: https://axios.nuxtjs.org/usage
'@nuxtjs/axios',
'@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'],
/*
@ -93,7 +109,6 @@ module.exports = {
},
buildModules: ['@nuxtjs/vuetify'],
vuetify: {
customVariables: ['~/assets/variables.scss'],
treeShake: true,
theme: {
options: {

View file

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

View file

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

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

View file

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

View file

@ -11,7 +11,7 @@
Search(
:filters='filters'
@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
//- v-tab {{$t('common.email')}}
@ -86,7 +86,7 @@ import { mapState } from 'vuex'
import FollowMe from '../components/FollowMe'
import Search from '@/components/Search'
import clipboard from '../assets/clipboard'
import { mdiContentCopy } from '@mdi/js'
import { mdiContentCopy, mdiChevronRight, mdiChevronLeft } from '@mdi/js'
export default {
name: 'Exports',
@ -104,7 +104,7 @@ export default {
},
data ({ $store }) {
return {
mdiContentCopy,
mdiContentCopy, mdiChevronLeft, mdiChevronRight,
type: 'rss',
notification: { email: '' },
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')
//- Calendar and search bar
v-row.pt-0.pt-sm-2.pl-0.pl-sm-2
#calh.col-xl-5.col-lg-5.col-md-7.col-sm-12.col-xs-12.pa-4.pa-sm-3
v-row.ma-2
#calh.col-xl-5.col-lg-5.col-md-7.col-sm-12.col-xs-12.pa-0.ma-0
//- this is needed as v-calendar does not support SSR
//- https://github.com/nathanreyes/v-calendar/issues/336
client-only(placeholder='Calendar unavailable without js')
Calendar(@dayclick='dayChange' @monthchange='monthChange' :events='filteredEvents')
client-only(placeholder='Loading...')
Calendar(@dayclick='dayChange' @monthchange='monthChange' :events='events')
.col.pt-0.pt-md-2
Search(:filters='filters' @update='updateFilters')
v-chip(v-if='selectedDay' close :close-icon='mdiCloseCircle' @click:close='dayChange()') {{selectedDay}}
.col.pt-0.pt-md-2.mt-4.ma-md-0.pb-0
//- v-btn(to='/search' color='primary' ) {{$t('common.search')}}
v-form(to='/search' action='/search' method='GET')
v-text-field(name='search' :label='$t("common.search")' outlined rounded hide-details :append-icon='mdiMagnify')
//- Events
#events.mb-2.mt-1.pl-1.pl-sm-2
Event(:event='event' @destroy='destroy' v-for='(event, idx) in visibleEvents' :lazy='idx>2' :key='event.id' @tagclick='tagClick' @placeclick='placeClick')
Event(:event='event' @destroy='destroy' v-for='(event, idx) in visibleEvents' :lazy='idx>2' :key='event.id')
</template>
<script>
import { mapState, mapActions } from 'vuex'
import intersection from 'lodash/intersection'
import { mapState } from 'vuex'
import dayjs from 'dayjs'
import Event from '@/components/Event'
import Announcement from '@/components/Announcement'
import Search from '@/components/Search'
import Calendar from '@/components/Calendar'
import { mdiCloseCircle } from '@mdi/js'
import { mdiMagnify } from '@mdi/js'
export default {
name: 'Index',
components: { Event, Search, Announcement, Calendar },
components: { Event, Announcement, Calendar },
middleware: 'setup',
async asyncData ({ params, $api, store }) {
async asyncData ({ $api }) {
const events = await $api.getEvents({
start: dayjs().startOf('month').unix(),
end: null,
@ -45,13 +43,13 @@ export default {
})
return { events }
},
data ({ $store }) {
data () {
return {
mdiCloseCircle,
mdiMagnify,
first: true,
isCurrentMonth: true,
now: dayjs().unix(),
date: dayjs().format('YYYY-MM-DD'),
date: dayjs.tz().format('YYYY-MM-DD'),
events: [],
start: dayjs().startOf('month').unix(),
end: null,
@ -74,49 +72,22 @@ export default {
]
}
},
computed: {
...mapState(['settings', 'announcements', 'filters']),
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
})
},
...mapState(['settings', 'announcements']),
visibleEvents () {
const now = dayjs().unix()
if (this.selectedDay) {
const min = dayjs(this.selectedDay).startOf('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) {
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 {
return this.filteredEvents
return this.events
}
}
},
methods: {
// onIntersect (isIntersecting, eventId) {
// this.intersecting[eventId] = isIntersecting
// },
...mapActions(['setFilters']),
destroy (id) {
this.events = this.events.filter(e => e.id !== id)
},
@ -131,20 +102,6 @@ export default {
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 }) {
// avoid first time monthChange event (onload)
if (this.first) {
@ -158,24 +115,20 @@ export default {
this.selectedDay = null
// 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.start = dayjs().startOf('month').unix()
this.date = dayjs().format('YYYY-MM-DD')
this.date = dayjs.tz().format('YYYY-MM-DD')
} else {
this.isCurrentMonth = false
this.date = ''
this.start = dayjs().year(year).month(month - 1).startOf('month').unix() // .startOf('week').unix()
}
// TODO: check if calendar view is double
this.end = dayjs().year(year).month(month).endOf('month').unix() // .endOf('week').unix()
this.updateEvents()
},
updateFilters (filters) {
this.setFilters(filters)
},
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">
v-container.pa-6
h2.mb-2.text-center Gancio Setup
v-stepper.grey.lighten-5(v-model='step')
@ -16,17 +15,12 @@
v-stepper-content(step='2')
Settings(setup, @complete='configCompleted')
v-stepper-content(step='3')
Completed(ref='completed')
Completed(ref='completed' :isHttp='isHttp')
</template>
<script>
import DbStep from './DbStep'
import Settings from '../../components/admin/Settings'
import Completed from './Completed'
import DbStep from '@/components/DbStep'
import Settings from '@/components/admin/Settings'
import Completed from '@/components/Completed'
export default {
components: { DbStep, Settings, Completed },
@ -36,9 +30,10 @@ export default {
title: 'Setup',
},
auth: false,
asyncData ({ params }) {
asyncData ({ params, req }) {
const protocol = process.client ? window.location.protocol : req.protocol + ':'
return {
isHttp: protocol === 'http:',
dbdone: !!Number(params.db),
config: {
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
* tags: [tag, list],
* places: [place_id],
* limit: (default )
* max: (default )
* }
*
*/
@ -22,7 +22,8 @@ export default ({ $axios }, inject) => {
end: params.end,
places: params.places && params.places.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))

View file

@ -7,12 +7,15 @@ import localizedFormat from 'dayjs/plugin/localizedFormat'
import 'dayjs/locale/it'
import 'dayjs/locale/en'
import 'dayjs/locale/es'
import 'dayjs/locale/ca'
import 'dayjs/locale/pl'
import 'dayjs/locale/eu'
import 'dayjs/locale/nb'
import 'dayjs/locale/fr'
import 'dayjs/locale/de'
import 'dayjs/locale/gl'
dayjs.extend(relativeTime)
dayjs.extend(utc)
@ -23,25 +26,27 @@ export default ({ app, store }) => {
// set timezone to instance_timezone!!
// to show local time relative to event's place
// not where in the world I'm looking at the page from
dayjs.tz.setDefault(store.state.settings.instance_timezone)
dayjs.locale(store.state.locale)
const instance_timezone = store.state.settings.instance_timezone
const locale = store.state.locale
dayjs.tz.setDefault(instance_timezone)
dayjs.locale(locale)
// replace links with anchors
// TODO: remove fb tracking id?
Vue.filter('linkify', value => value.replace(/(https?:\/\/([^\s]+))/g, '<a href="$1">$2</a>'))
Vue.filter('url2host', url => url.match(/^https?:\/\/(.[^/:]+)/i)[1])
Vue.filter('datetime', value => dayjs(value).locale(store.state.locale).format('ddd, D MMMM HH:mm'))
Vue.filter('dateFormat', (value, format) => dayjs(value).format(format))
Vue.filter('unixFormat', (timestamp, format) => dayjs.unix(timestamp).format(format))
Vue.filter('datetime', value => dayjs.tz(value).locale(locale).format('ddd, D MMMM HH:mm'))
Vue.filter('dateFormat', (value, format) => dayjs.tz(value).format(format))
Vue.filter('unixFormat', (timestamp, format) => dayjs.unix(timestamp).tz(instance_timezone).format(format))
// shown in mobile homepage
Vue.filter('day', value => dayjs.unix(value).locale(store.state.locale).format('dddd, D MMM'))
Vue.filter('mediaURL', (event, type) => {
Vue.filter('day', value => dayjs.unix(value).tz(instance_timezone).locale(store.state.locale).format('dddd, D MMM'))
Vue.filter('mediaURL', (event, type, format = '.jpg') => {
if (event.media && event.media.length) {
if (type === 'alt') {
return event.media[0].name
} else {
return store.state.settings.baseurl + '/media/' + (type === 'thumb' ? 'thumb/' : '') + event.media[0].url
return store.state.settings.baseurl + '/media/' + (type === 'thumb' ? 'thumb/' : '') + event.media[0].url.replace(/.jpg$/, format)
}
} else if (type !== 'alt') {
return store.state.settings.baseurl + '/media/' + (type === 'thumb' ? 'thumb/' : '') + 'logo.svg'
@ -49,16 +54,16 @@ export default ({ app, store }) => {
return ''
})
Vue.filter('from', timestamp => dayjs.unix(timestamp).fromNow())
Vue.filter('from', timestamp => dayjs.unix(timestamp).tz(instance_timezone).fromNow())
Vue.filter('recurrentDetail', event => {
const parent = event.parent
const { frequency, type } = parent.recurrent
let recurrent
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') {
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') {
recurrent = app.i18n.t(`event.recurrent_${frequency}_days`, { days: d })
} else {
@ -70,8 +75,8 @@ export default ({ app, store }) => {
})
Vue.filter('when', (event) => {
const start = dayjs.unix(event.start_datetime)
const end = dayjs.unix(event.end_datetime)
const start = dayjs.unix(event.start_datetime).tz(instance_timezone)
const end = dayjs.unix(event.end_datetime).tz(instance_timezone)
// const normal = `${start.format('dddd, D MMMM (HH:mm-')}${end.format('HH:mm) ')}`
// // recurrent event
@ -90,10 +95,10 @@ export default ({ app, store }) => {
// 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
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 dayjs = require('dayjs')
const helpers = require('../../helpers')
const settingsController = require('./settings')
const Event = require('../models/event')
const Resource = require('../models/resource')
@ -23,31 +22,110 @@ const log = require('../../log')
const eventController = {
async _getMeta () {
async searchMeta (req, res) {
const search = req.query.search
const places = await Place.findAll({
order: [[Sequelize.literal('weigth'), 'DESC']],
attributes: {
include: [[Sequelize.fn('count', Sequelize.col('events.placeId')), 'weigth']],
exclude: ['createdAt', 'updatedAt']
order: [[Sequelize.col('w'), 'DESC']],
where: {
[Op.or]: [
{ 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: [] }],
group: ['place.id']
group: ['place.id'],
raw: true
})
const tags = await Tag.findAll({
order: [[Sequelize.literal('w'), 'DESC']],
attributes: {
include: [[Sequelize.fn('COUNT', Sequelize.col('tag.tag')), 'w']]
order: [[Sequelize.col('w'), 'DESC']],
where: {
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 }],
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) {
@ -75,14 +153,7 @@ const eventController = {
const notifications = await Notification.findAll({ where: { action }, include: [Event] })
// get notification that matches with selected event
const ret = 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)
return notifications.filter(notification => match(event, notification.filters))
},
async _get(slug) {
@ -290,8 +361,8 @@ const eventController = {
res.sendStatus(200)
},
async isAnonEventAllowed (req, res, next) {
if (!res.locals.settings.allow_anon_event) {
async isAnonEventAllowed (_req, res, next) {
if (!res.locals.settings.allow_anon_event && !res.locals.user) {
return res.sendStatus(403)
}
next()
@ -308,16 +379,33 @@ const eventController = {
const body = req.body
const recurrent = body.recurrent ? JSON.parse(body.recurrent) : null
const required_fields = [ 'title', 'place_name', 'start_datetime']
const missing_field = required_fields.find(required_field => !body[required_field])
const required_fields = [ 'title', 'start_datetime']
let missing_field = required_fields.find(required_field => !body[required_field])
if (missing_field) {
log.warn(`${missing_field} is required`)
return res.status(400).send(`${missing_field} is required`)
log.warn(`${missing_field} 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 = {
title: body.title,
// remove html tags
// sanitize and linkify html
description: helpers.sanitizeHTML(linkifyHtml(body.description || '')),
multidate: body.multidate,
start_datetime: body.start_datetime,
@ -328,17 +416,16 @@ const eventController = {
}
if (req.file || body.image_url) {
let url
if (req.file) {
url = req.file.filename
} else {
url = await helpers.getImageFromURL(body.image_url)
if (!req.file && body.image_url) {
req.file = await helpers.getImageFromURL(body.image_url)
}
let focalpoint = body.image_focalpoint ? body.image_focalpoint.split(',') : ['0', '0']
focalpoint = [parseFloat(focalpoint[0]).toFixed(2), parseFloat(focalpoint[1]).toFixed(2)]
eventDetails.media = [{
url,
url: req.file.filename,
height: req.file.height,
width: req.file.width,
name: body.image_name || body.title || '',
focalpoint: [parseFloat(focalpoint[0]), parseFloat(focalpoint[1])]
}]
@ -346,24 +433,16 @@ const eventController = {
eventDetails.media = []
}
const event = await Event.create(eventDetails)
const [place] = await Place.findOrCreate({
where: { name: body.place_name },
defaults: {
address: body.place_address
}
})
let event = await Event.create(eventDetails)
await event.setPlace(place)
event.place = place
// create/assign tags
if (body.tags) {
body.tags = body.tags.map(t => t.trim())
await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true })
const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } })
await event.addTags(tags)
event.tags = tags
}
// associate user to event and reverse
@ -372,6 +451,9 @@ const eventController = {
await event.setUser(res.locals.user)
}
event = event.get()
event.tags = body.tags
event.place = place
// return created event to the client
res.json(event)
@ -405,47 +487,44 @@ const eventController = {
const recurrent = body.recurrent ? JSON.parse(body.recurrent) : null
const eventDetails = {
title: body.title,
// remove html tags
description: helpers.sanitizeHTML(linkifyHtml(body.description, { target: '_blank' })),
title: body.title || event.title,
// sanitize and linkify html
description: helpers.sanitizeHTML(linkifyHtml(body.description, { target: '_blank' })) || event.description,
multidate: body.multidate,
start_datetime: body.start_datetime,
end_datetime: body.end_datetime,
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) {
try {
const old_path = path.resolve(config.upload_path, event.media[0].url)
const old_thumb_path = path.resolve(config.upload_path, 'thumb', event.media[0].url)
try {
fs.unlinkSync(old_path)
fs.unlinkSync(old_thumb_path)
} catch (e) {
log.info(e.toString())
}
}
let url
if (req.file) {
url = req.file.filename
} else if (body.image_url) {
if (/^https?:\/\//.test(body.image_url)) {
url = await helpers.getImageFromURL(body.image_url)
} else {
url = body.image_url
}
// modify associated media only if a new file is uploaded or remote image_url is used
if (req.file || (body.image_url && /^https?:\/\//.test(body.image_url))) {
if (body.image_url) {
req.file = await helpers.getImageFromURL(body.image_url)
}
if (url && !event.recurrent) {
const focalpoint = body.image_focalpoint ? body.image_focalpoint.split(',') : ['0', '0']
eventDetails.media = [{
url,
name: body.image_name || '',
url: req.file.filename,
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))]
}]
} else {
} else if (!body.image) {
eventDetails.media = []
}
await event.update(eventDetails)
const [place] = await Place.findOrCreate({
where: { name: body.place_name },
@ -481,9 +560,9 @@ const eventController = {
// check if event is mine (or user is admin)
if (event && (res.locals.user.is_admin || res.locals.user.id === event.userId)) {
if (event.media && event.media.length && !event.recurrent) {
try {
const old_path = path.join(config.upload_path, event.media[0].url)
const old_thumb_path = path.join(config.upload_path, 'thumb', event.media[0].url)
try {
fs.unlinkSync(old_thumb_path)
fs.unlinkSync(old_path)
} catch (e) {
@ -523,22 +602,22 @@ const eventController = {
if (!show_recurrent) {
where.parentId = null
}
if (end) {
where.start_datetime = { [Op.lte]: end }
}
const replacements = []
if (tags && places) {
where[Op.or] = {
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)} ) )`)
}
}
if (tags) {
where['$tags.tag$'] = tags.split(',')
}
if (places) {
} else if (tags) {
// where[Op.and] = Sequelize.literal(`EXISTS (SELECT 1 FROM event_tags WHERE eventId=event.id AND tagTag in (?))`)
where[Op.and] = Sequelize.fn('EXISTS', Sequelize.literal('SELECT 1 FROM event_tags WHERE "event_tags"."eventId"="event".id AND "tagTag" in (?)'))
replacements.push(tags)
} else if (places) {
where.placeId = places.split(',')
}
@ -554,12 +633,12 @@ const eventController = {
model: Tag,
order: [Sequelize.literal('(SELECT COUNT("tagTag") FROM event_tags WHERE tagTag = tag) DESC')],
attributes: ['tag'],
required: !!tags,
through: { attributes: [] }
},
{ model: Place, required: true, attributes: ['id', 'name', 'address'] }
],
limit: max
limit: max,
replacements
}).catch(e => {
log.error('[EVENT]', e)
return []

View file

@ -2,6 +2,7 @@ const Event = require('../models/event')
const Place = require('../models/place')
const Tag = require('../models/tag')
const { htmlToText } = require('html-to-text')
const { Op, literal } = require('sequelize')
const moment = require('dayjs')
const ics = require('ics')
@ -68,7 +69,7 @@ const exportController = {
}
},
feed (req, res, events) {
feed (_req, res, events) {
const settings = res.locals.settings
res.type('application/rss+xml; charset=UTF-8')
res.render('feed/rss.pug', { events, settings, moment })
@ -79,7 +80,7 @@ const exportController = {
* @param {*} events array of events from sequelize
* @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 eventsMap = events.map(e => {
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)
return {
start,
// startOutputType: 'utc',
end,
// endOutputType: 'utc',
title: `[${settings.title}] ${e.title}`,
description: e.description,
description: htmlToText(e.description),
htmlContent: e.description,
location: `${e.place.name} - ${e.place.address}`,
url: `${settings.baseurl}/event/${e.slug || e.id}`,
status: 'CONFIRMED',
categories: e.tags.map(t => t.tag),
alarms
}
})

View file

@ -137,8 +137,7 @@ const oauthController = {
code.userId = user.id
code.clientId = client.id
code.expiresAt = dayjs(code.expiresAt).toDate()
const ret = await OAuthCode.create(code)
return ret
return OAuthCode.create(code)
},
// 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 sharp = require('sharp')
const config = require('../../config')
const pkg = require('../../../package.json')
const generateKeyPair = promisify(crypto.generateKeyPair)
const log = require('../../log')
const locales = require('../../../locales/index')
@ -42,7 +41,7 @@ const defaultSettings = {
{ href: '/about', label: 'about' }
],
admin_email: config.admin_email || '',
smtp: config.smtp || false
smtp: config.smtp || {}
}
/**
@ -185,7 +184,7 @@ const settingsController = {
return sharp(uploadedPath)
.resize(400)
.png({ quality: 90 })
.toFile(baseImgPath + '.png', (err, info) => {
.toFile(baseImgPath + '.png', (err) => {
if (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) {
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()
log.warn(`User ${user.email} removed!`)
res.sendStatus(200)

View file

@ -23,6 +23,8 @@ if (config.status !== 'READY') {
const { isAuth, isAdmin } = require('./auth')
const eventController = require('./controller/event')
const placeController = require('./controller/place')
const tagController = require('./controller/tag')
const settingsController = require('./controller/settings')
const exportController = require('./controller/export')
const userController = require('./controller/user')
@ -31,6 +33,7 @@ if (config.status !== 'READY') {
const resourceController = require('./controller/resource')
const oauthController = require('./controller/oauth')
const announceController = require('./controller/announce')
const cohortController = require('./controller/cohort')
const helpers = require('../helpers')
const storage = require('./storage')
const upload = multer({ storage })
@ -72,14 +75,11 @@ if (config.status !== 'READY') {
// delete user
api.delete('/user/:id', isAdmin, userController.remove)
api.delete('/user', isAdmin, userController.remove)
api.delete('/user', isAuth, userController.remove)
// get all users
api.get('/users', isAdmin, userController.getAll)
// update a place (modify address..)
api.put('/place', isAdmin, eventController.updatePlace)
/**
* Get events
* @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)
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.get('/event/import', isAuth, helpers.importURL)
@ -127,7 +129,7 @@ if (config.status !== 'READY') {
api.delete('/event/:id', isAuth, eventController.remove)
// get tags/places
api.get('/event/meta', eventController.getMeta)
api.get('/event/meta', eventController.searchMeta)
// get unconfirmed events
api.get('/event/unconfirmed', isAdmin, eventController.getUnconfirmed)
@ -150,6 +152,15 @@ if (config.status !== 'READY') {
// export events (rss/ics)
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/:instance_domain', isAdmin, instanceController.get)
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.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
api.get('/clients', isAuth, oauthController.getClients)
api.get('/client/:client_id', isAuth, oauthController.getClient)
api.post('/client', oauthController.createClient)
}
api.use((req, res) => res.sendStatus(404))
api.use((_req, res) => res.sendStatus(404))
// Handle 500
api.use((error, req, res, next) => {
api.use((error, _req, res, _next) => {
log.error('[API ERROR]', error)
res.status(500).send('500: Internal Server Error')
})

View file

@ -9,7 +9,7 @@ const locales = require('../../locales')
const mail = {
send (addresses, template, locals, 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!`)
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 moment = require('dayjs')
const { htmlToText } = require('html-to-text')
const { Model, DataTypes } = require('sequelize')
@ -14,9 +13,12 @@ const Place = require('./place')
const User = require('./user')
const Tag = require('./tag')
const utc = require('dayjs/plugin/utc')
const dayjs = require('dayjs')
const timezone = require('dayjs/plugin/timezone')
const utc = require('dayjs/plugin/utc')
dayjs.extend(utc)
dayjs.extend(timezone)
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 content = `
📍 ${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}
`
@ -99,8 +101,8 @@ Event.prototype.toAP = function (username, locale, to = []) {
name: this.title,
url: `${config.baseurl}/event/${this.slug || this.id}`,
type: 'Event',
startTime: moment.unix(this.start_datetime).locale(locale).format(),
endTime: this.end_datetime ? moment.unix(this.end_datetime).locale(locale).format() : null,
startTime: dayjs.unix(this.start_datetime).tz().locale(locale).format(),
endTime: this.end_datetime ? dayjs.unix(this.end_datetime).tz().locale(locale).format() : null,
location: {
name: this.place.name,
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) {
log.debug(`Connecting to DB: ${JSON.stringify(dbConf)}`)
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)
return db.sequelize.authenticate()
},
@ -39,7 +50,7 @@ const db = {
path: path.resolve(__dirname, '..', '..', 'migrations')
}
})
return await umzug.up()
return umzug.up()
},
async initialize () {
if (config.status === 'READY') {

View file

@ -12,48 +12,33 @@ try {
const DiskStorage = {
_handleFile (req, file, cb) {
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 filename = crypto.randomBytes(16).toString('hex')
const sharpStream = sharp({ failOnError: true })
const promises = [
sharpStream.clone().resize(500, null, { withoutEnlargement: true }).jpeg({ mozjpeg: true, progressive: true }).toFile(path.resolve(config.upload_path, 'thumb', filename + '.jpg')),
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 })
const thumbnailer = sharp().resize(500).jpeg({ quality: 98 })
let onError = false
const err = e => {
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', () => {
file.stream.pipe(sharpStream)
Promise.all(promises)
.then(res => {
const info = res[1]
cb(null, {
destination: config.upload_path,
filename,
path: finalPath,
size: outStream.bytesWritten
filename: filename + '.jpg',
path: path.resolve(config.upload_path, filename + '.jpg'),
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.filename
fs.unlink(file.path, cb)

View file

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

View file

@ -1,8 +1,9 @@
let db
function _initializeDB () {
const config = require('../config')
config.load()
config.log_level = 'error'
const db = require('../api/models/index')
db = require('../api/models/index')
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 () {
@ -33,22 +57,31 @@ async function list () {
const User = require('../api/models/user')
const users = await User.findAll()
console.log()
users.forEach(u => console.log(`${u.id}\tadmin: ${u.is_admin}\tenabled: ${u.is_active}\temail: ${u.email} - ${u.password}`))
users.forEach(u => console.log(`${u.id}\tadmin: ${u.is_admin}\tenabled: ${u.is_active}\temail: ${u.email}`))
console.log()
await db.close()
}
const accountsCLI = yargs => {
return yargs
const accountsCLI = yargs => yargs
.command('list', 'List all accounts', list)
.command('modify', 'Modify', {
account: {
describe: 'Account to modify'
describe: 'Account to modify',
type: 'string',
demandOption: true
},
'reset-password': {
describe: 'Resets the password of the given accoun '
describe: 'Resets the password of the given account ',
type: 'boolean'
}
}, 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

View file

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

View file

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

View file

@ -20,7 +20,7 @@ const log = require('../log')
router.use(cors())
// is federation enabled? middleware
router.use((req, res, next) => {
router.use((_req, res, next) => {
if (settingsController.settings.enable_federation) { return next() }
log.debug('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.get('/m/:event_id', async (req, res) => {
const settingsController = require('../api/controller/settings')
log.debug('[AP] Get event details ')
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] })
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

View file

@ -5,6 +5,7 @@ const APUser = require('../api/models/ap_user')
const log = require('../log')
const helpers = require('../helpers')
const linkifyHtml = require('linkify-html')
const get = require('lodash/get')
module.exports = {
@ -59,7 +60,7 @@ module.exports = {
async remove (req, res) {
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'] }]
})
if (!resource) {

View file

@ -28,7 +28,7 @@ router.get('/webfinger', allowFederation, (req, res) => {
}
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:(.*)@(.*)/)
if (domain !== req_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 log = require('./log')
const pkg = require('../package.json')
const fs = require('fs')
const path = require('path')
const sharp = require('sharp')
const axios = require('axios')
@ -95,8 +94,8 @@ module.exports = {
serveStatic () {
const router = express.Router()
// serve event's images/thumb
router.use('/media/', express.static(config.upload_path, { immutable: true, maxAge: '1y' } ))
// serve images/thumb
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('/logo.png', (req, res, next) => {
@ -112,7 +111,7 @@ module.exports = {
return router
},
logRequest (req, res, next) {
logRequest (req, _res, next) {
log.debug(`${req.method} ${req.path}`)
next()
},
@ -122,40 +121,33 @@ module.exports = {
if(!/^https?:\/\//.test(url)) {
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 thumbnailer = sharp().resize(400).jpeg({ quality: 90 })
const filename = crypto.randomBytes(16).toString('hex')
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) => {
let onError = false
const err = e => {
if (onError) {
return
response.data.pipe(sharpStream)
return Promise.all(promises)
.then(res => {
const info = res[1]
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)
}
response.data
.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))
})
.catch(err => {
log.error(err)
req.err = err
cb(null)
})
},
@ -232,7 +224,8 @@ module.exports = {
},
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 event = await eventController._get(req.params.slug)
if (event) {

View file

@ -1,14 +1,29 @@
module.exports = function () {
const config = require('../server/config')
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 settingsController = require('./api/controller/settings')
const db = require('./api/models/index')
const dayjs = require('dayjs')
const timezone = require('dayjs/plugin/timezone')
async function start (nuxt) {
dayjs.extend(timezone)
if (config.status == 'READY') {
await db.initialize()
} else {
@ -18,18 +33,21 @@ module.exports = function () {
dialect: process.env.GANCIO_DB_DIALECT,
storage: process.env.GANCIO_DB_STORAGE,
host: process.env.GANCIO_DB_HOST,
port: process.env.GANCIO_DB_PORT,
database: process.env.GANCIO_DB_DATABASE,
username: process.env.GANCIO_DB_USERNAME,
password: process.env.GANCIO_DB_PASSWORD,
}
setupController._setupDb(dbConf)
.catch(e => { process.exit(1) })
.catch(e => {
log.warn(String(e))
process.exit(1)
})
}
await settingsController.load()
}
dayjs.extend(timezone)
dayjs.tz.setDefault(settingsController.settings.instance_timezone)
let TaskManager
@ -37,22 +55,11 @@ module.exports = function () {
TaskManager = require('../server/taskManager').TaskManager
TaskManager.start()
}
log.info(`Listen on ${config.server.host}:${config.server.port}`)
// close connections/port/unix socket
async function 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', initialize.shutdown)
process.on('SIGINT', initialize.shutdown)
}
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 cookieParser = require('cookie-parser')
const initialize = require('./initialize.server')
initialize.start()
// const metricsController = require('./metrics')
// const promBundle = require('express-prom-bundle')
// const metricsMiddleware = promBundle({ includeMethod: true })
@ -34,11 +37,12 @@ if (config.status === 'READY') {
// rss/ics/atom feed
app.get('/feed/:type', cors(), exportController.export)
app.use('/.well-known', webfinger)
app.use('/event/:slug', helpers.APRedirect)
// federation api / activitypub / webfinger / nodeinfo
app.use('/federation', federation)
app.use('/.well-known', webfinger)
// ignore unimplemented ping url from fediverse
app.use(spamFilter)
@ -54,26 +58,34 @@ if (config.status === 'READY') {
app.use('/api', api)
// // Handle 500
app.use((error, req, res, next) => {
app.use((error, _req, res, _next) => {
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
// first nuxt component is ./pages/index.vue (with ./layouts/default.vue)
// prefill current events, tags, places and announcements (used in every path)
app.use(async (req, res, next) => {
// const start_datetime = getUnixTime(startOfWeek(startOfMonth(new Date())))
// req.events = await eventController._select(start_datetime, 100)
if (config.status === 'READY') {
const eventController = require('./api/controller/event')
const announceController = require('./api/controller/announce')
res.locals.meta = await eventController._getMeta()
res.locals.announcements = await announceController._getVisible()
}
res.locals.status = config.status
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();
}
function blank_object() {
return Object.create(null);
return /* @__PURE__ */ Object.create(null);
}
function run_all(fns) {
fns.forEach(run);
@ -104,7 +104,7 @@ function schedule_update() {
function add_render_callback(fn) {
render_callbacks.push(fn);
}
const seen_callbacks = new Set();
const seen_callbacks = /* @__PURE__ */ new Set();
let flushidx = 0;
function flush() {
const saved_component = current_component;
@ -146,7 +146,7 @@ function update($$) {
$$.after_update.forEach(add_render_callback);
}
}
const outroing = new Set();
const outroing = /* @__PURE__ */ new Set();
function transition_in(block, local) {
if (block && block.i) {
outroing.delete(block);
@ -282,19 +282,41 @@ if (typeof HTMLElement === "function") {
}
function get_each_context(ctx, list, i) {
const child_ctx = ctx.slice();
child_ctx[11] = list[i];
child_ctx[12] = list[i];
return child_ctx;
}
function get_each_context_1(ctx, list, i) {
const child_ctx = ctx.slice();
child_ctx[14] = list[i];
child_ctx[15] = list[i];
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) {
let div;
let t;
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 = [];
for (let i = 0; i < each_value.length; i += 1) {
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 = null;
}
if (dirty & 25) {
each_value = ctx2[4];
if (dirty & 41) {
each_value = ctx2[5];
let i;
for (i = 0; i < each_value.length; i += 1) {
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(img, "id", "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(div1, "class", "content");
attr(a, "href", ctx[0]);
@ -413,7 +435,7 @@ function create_if_block_4(ctx) {
p(ctx2, dirty) {
if (dirty & 2)
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);
}
if (dirty & 1) {
@ -429,7 +451,7 @@ function create_if_block_4(ctx) {
function create_if_block_2(ctx) {
let div;
function select_block_type(ctx2, dirty) {
if (ctx2[11].media.length)
if (ctx2[12].media.length)
return create_if_block_3;
return create_else_block;
}
@ -472,7 +494,7 @@ function create_else_block(ctx) {
c() {
img = element("img");
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"))
attr(img, "src", img_src_value);
attr(img, "loading", "lazy");
@ -481,7 +503,7 @@ function create_else_block(ctx) {
insert(target, img, anchor);
},
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);
}
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 {
c() {
img = element("img");
attr(img, "style", img_style_value = "object-position: " + position$1(ctx[11]) + "; aspect-ratio=1.7778;");
attr(img, "alt", img_alt_value = ctx[11].media[0].name);
if (!src_url_equal(img.src, img_src_value = ctx[0] + "/media/thumb/" + ctx[11].media[0].url))
attr(img, "style", img_style_value = "object-position: " + position$1(ctx[12]) + "; aspect-ratio=1.7778;");
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[12].media[0].url))
attr(img, "src", img_src_value);
attr(img, "loading", "lazy");
},
@ -512,13 +534,13 @@ function create_if_block_3(ctx) {
insert(target, img, anchor);
},
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);
}
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);
}
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);
}
},
@ -530,7 +552,7 @@ function create_if_block_3(ctx) {
}
function create_if_block_1$1(ctx) {
let div;
let each_value_1 = ctx[11].tags;
let each_value_1 = ctx[12].tags;
let each_blocks = [];
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));
@ -550,8 +572,8 @@ function create_if_block_1$1(ctx) {
}
},
p(ctx2, dirty) {
if (dirty & 16) {
each_value_1 = ctx2[11].tags;
if (dirty & 32) {
each_value_1 = ctx2[12].tags;
let i;
for (i = 0; i < each_value_1.length; i += 1) {
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) {
let span;
let t0;
let t1_value = ctx[14] + "";
let t1_value = ctx[15] + "";
let t1;
return {
c() {
@ -594,7 +616,7 @@ function create_each_block_1(ctx) {
append(span, t1);
},
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);
},
d(detaching) {
@ -608,27 +630,27 @@ function create_each_block(ctx) {
let t0;
let div2;
let div0;
let t1_value = when$1(ctx[11].start_datetime) + "";
let t1_value = when$1(ctx[12].start_datetime) + "";
let t1;
let t2;
let div1;
let t3_value = ctx[11].title + "";
let t3_value = ctx[12].title + "";
let t3;
let t4;
let span1;
let t5;
let t6_value = ctx[11].place.name + "";
let t6_value = ctx[12].place.name + "";
let t6;
let t7;
let span0;
let t8_value = ctx[11].place.address + "";
let t8_value = ctx[12].place.address + "";
let t8;
let t9;
let t10;
let a_href_value;
let a_title_value;
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 {
c() {
a = element("a");
@ -657,9 +679,9 @@ function create_each_block(ctx) {
attr(span0, "class", "subtitle");
attr(span1, "class", "place");
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, "title", a_title_value = ctx[11].title);
attr(a, "title", a_title_value = ctx[12].title);
attr(a, "target", "_blank");
},
m(target, anchor) {
@ -698,15 +720,15 @@ function create_each_block(ctx) {
if_block0.d(1);
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);
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);
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);
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);
if (ctx2[11].tags.length) {
if (ctx2[12].tags.length) {
if (if_block1) {
if_block1.p(ctx2, dirty);
} else {
@ -718,10 +740,10 @@ function create_each_block(ctx) {
if_block1.d(1);
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);
}
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);
}
},
@ -736,41 +758,65 @@ function create_each_block(ctx) {
};
}
function create_fragment$1(ctx) {
let if_block_anchor;
let if_block = ctx[4].length && create_if_block$1(ctx);
let t;
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 {
c() {
if (if_block)
if_block.c();
if_block_anchor = empty();
if (if_block0)
if_block0.c();
t = space();
if (if_block1)
if_block1.c();
if_block1_anchor = empty();
this.c = noop;
},
m(target, anchor) {
if (if_block)
if_block.m(target, anchor);
insert(target, if_block_anchor, anchor);
if (if_block0)
if_block0.m(target, anchor);
insert(target, t, anchor);
if (if_block1)
if_block1.m(target, anchor);
insert(target, if_block1_anchor, anchor);
},
p(ctx2, [dirty]) {
if (ctx2[4].length) {
if (if_block) {
if_block.p(ctx2, dirty);
if (ctx2[4]) {
if (if_block0) {
if_block0.p(ctx2, dirty);
} else {
if_block = create_if_block$1(ctx2);
if_block.c();
if_block.m(if_block_anchor.parentNode, if_block_anchor);
if_block0 = create_if_block_5(ctx2);
if_block0.c();
if_block0.m(t.parentNode, t);
}
} else if (if_block) {
if_block.d(1);
if_block = null;
} else if (if_block0) {
if_block0.d(1);
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,
o: noop,
d(detaching) {
if (if_block)
if_block.d(detaching);
if (if_block0)
if_block0.d(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 { show_recurrent = false } = $$props;
let { sidebar = "true" } = $$props;
let { external_style = "" } = $$props;
let mounted = false;
let events = [];
function update2(v) {
@ -814,11 +861,9 @@ function instance$1($$self, $$props, $$invalidate) {
if (places) {
params.push(`places=${places}`);
}
if (show_recurrent) {
params.push(`show_recurrent=true`);
}
params.push(`show_recurrent=${show_recurrent ? "true" : "false"}`);
fetch(`${baseurl}/api/events?${params.join("&")}`).then((res) => res.json()).then((e) => {
$$invalidate(4, events = e);
$$invalidate(5, events = e);
}).catch((e) => {
console.error("Error loading Gancio API -> ", e);
});
@ -833,20 +878,22 @@ function instance$1($$self, $$props, $$invalidate) {
if ("title" in $$props2)
$$invalidate(1, title = $$props2.title);
if ("maxlength" in $$props2)
$$invalidate(5, maxlength = $$props2.maxlength);
$$invalidate(6, maxlength = $$props2.maxlength);
if ("tags" in $$props2)
$$invalidate(6, tags = $$props2.tags);
$$invalidate(7, tags = $$props2.tags);
if ("places" in $$props2)
$$invalidate(7, places = $$props2.places);
$$invalidate(8, places = $$props2.places);
if ("theme" in $$props2)
$$invalidate(2, theme = $$props2.theme);
if ("show_recurrent" in $$props2)
$$invalidate(8, show_recurrent = $$props2.show_recurrent);
$$invalidate(9, show_recurrent = $$props2.show_recurrent);
if ("sidebar" in $$props2)
$$invalidate(3, sidebar = $$props2.sidebar);
if ("external_style" in $$props2)
$$invalidate(4, external_style = $$props2.external_style);
};
$$self.$$.update = () => {
if ($$self.$$.dirty & 494) {
if ($$self.$$.dirty & 974) {
update2();
}
};
@ -855,6 +902,7 @@ function instance$1($$self, $$props, $$invalidate) {
title,
theme,
sidebar,
external_style,
events,
maxlength,
tags,
@ -873,12 +921,13 @@ class GancioEvents extends SvelteElement {
}, instance$1, create_fragment$1, safe_not_equal, {
baseurl: 0,
title: 1,
maxlength: 5,
tags: 6,
places: 7,
maxlength: 6,
tags: 7,
places: 8,
theme: 2,
show_recurrent: 8,
sidebar: 3
show_recurrent: 9,
sidebar: 3,
external_style: 4
}, null);
if (options) {
if (options.target) {
@ -899,7 +948,8 @@ class GancioEvents extends SvelteElement {
"places",
"theme",
"show_recurrent",
"sidebar"
"sidebar",
"external_style"
];
}
get baseurl() {
@ -917,21 +967,21 @@ class GancioEvents extends SvelteElement {
flush();
}
get maxlength() {
return this.$$.ctx[5];
return this.$$.ctx[6];
}
set maxlength(maxlength) {
this.$$set({ maxlength });
flush();
}
get tags() {
return this.$$.ctx[6];
return this.$$.ctx[7];
}
set tags(tags) {
this.$$set({ tags });
flush();
}
get places() {
return this.$$.ctx[7];
return this.$$.ctx[8];
}
set places(places) {
this.$$set({ places });
@ -945,7 +995,7 @@ class GancioEvents extends SvelteElement {
flush();
}
get show_recurrent() {
return this.$$.ctx[8];
return this.$$.ctx[9];
}
set show_recurrent(show_recurrent) {
this.$$set({ show_recurrent });
@ -958,6 +1008,13 @@ class GancioEvents extends SvelteElement {
this.$$set({ sidebar });
flush();
}
get external_style() {
return this.$$.ctx[4];
}
set external_style(external_style) {
this.$$set({ external_style });
flush();
}
}
customElements.define("gancio-events", GancioEvents);
function create_if_block(ctx) {
@ -996,7 +1053,7 @@ function create_if_block(ctx) {
t6 = text(t6_value);
attr(div1, "class", "place");
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, "target", "_blank");
},
@ -1035,7 +1092,7 @@ function create_if_block(ctx) {
set_data(t3, t3_value);
if (dirty & 2 && t6_value !== (t6_value = ctx2[1].place.name + ""))
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);
}
},

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