Merge branch 'master' into gh
This commit is contained in:
commit
616c54229a
110 changed files with 5909 additions and 2199 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -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
|
||||
|
|
34
CHANGELOG
34
CHANGELOG
|
@ -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
|
||||
|
|
6
app/router.scrollBehavior.js
Normal file
6
app/router.scrollBehavior.js
Normal 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
1262
assets/gancio-events.es.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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)
|
||||
let attributes = []
|
||||
attributes.push({ key: 'today', dates: new Date(), bar: { color: 'green', fillMode: 'outline' } })
|
||||
const now = dayjs().unix()
|
||||
export function attributesFromEvents (_events) {
|
||||
|
||||
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'
|
||||
}
|
||||
} else {
|
||||
color.class += ' vc-past'
|
||||
}
|
||||
// const colors = ['teal', 'green', 'yellow', 'teal', 'indigo', 'green', 'red', 'purple', 'pink', 'gray']
|
||||
// merge events with same date
|
||||
let attributes = []
|
||||
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' : ''
|
||||
|
||||
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
|
||||
}
|
||||
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.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 {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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,42 +67,32 @@ li {
|
|||
margin-right: .4em;
|
||||
transition: all .5s;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
margin: 0.5rem 1rem 0.5rem 1rem;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
font-size: 1.1em !important;
|
||||
line-height: 1.2em !important;
|
||||
}
|
||||
.event .title {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
margin: 0.5rem 1rem 0.5rem 1rem;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
font-size: 1.1em !important;
|
||||
line-height: 1.2em !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.body {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.event .place span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.place {
|
||||
max-width: 100%;
|
||||
span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.event a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.vc-past {
|
|
@ -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';
|
|
@ -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'
|
||||
|
|
|
@ -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
45
components/Completed.vue
Normal 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>
|
|
@ -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
|
||||
:disabled='!value.from'
|
||||
:rules="[$validators.required('event.from')]"
|
||||
:items='hourList' @change='hr => change("fromHour", hr)')
|
||||
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')]"
|
||||
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')"
|
||||
:disabled='!fromHour'
|
||||
:value='dueHour' clearable
|
||||
:items='hourList' @change='hr => change("dueHour", hr)')
|
||||
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'
|
||||
: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 })
|
||||
}
|
|
@ -176,7 +176,7 @@ export default {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
<style lang='less'>
|
||||
<style lang='scss'>
|
||||
|
||||
.editor {
|
||||
margin-top: 4px;
|
||||
|
|
|
@ -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')
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,6 @@ export default {
|
|||
event: {}
|
||||
}
|
||||
},
|
||||
computed: mapState(['places']),
|
||||
methods: {
|
||||
importGeneric () {
|
||||
if (this.file) {
|
|
@ -61,12 +61,12 @@ export default {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
<style lang='less'>
|
||||
#list {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
.v-list-item__title {
|
||||
white-space: normal !important;
|
||||
}
|
||||
}
|
||||
<style>
|
||||
#list {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
#list .v-list-item__title {
|
||||
white-space: normal !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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
97
components/MyPicture.vue
Normal 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>
|
|
@ -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')
|
||||
|
|
|
@ -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-model='showRecurrent'
|
||||
inset color='primary'
|
||||
hide-details
|
||||
:label="$t('event.show_recurrent')")
|
||||
v-autocomplete(
|
||||
:label='$t("common.search")'
|
||||
:items='keywords'
|
||||
hide-details
|
||||
@change='change'
|
||||
:value='selectedFilters'
|
||||
clearable
|
||||
:search-input.sync='search'
|
||||
item-text='label'
|
||||
return-object
|
||||
chips single-line
|
||||
multiple)
|
||||
template(v-slot:selection="data")
|
||||
v-chip(v-bind="data.attrs"
|
||||
close
|
||||
:close-icon='mdiCloseCircle'
|
||||
@click:close='remove(data.item)'
|
||||
:input-value="data.selected")
|
||||
v-avatar(left)
|
||||
v-icon(v-text="data.item.type === 'place' ? mdiMapMarker : mdiTag")
|
||||
span {{ data.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-container.pt-0.pt-md-2
|
||||
v-switch.mt-0(
|
||||
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")'
|
||||
:filter='filter'
|
||||
cache-items
|
||||
hide-details
|
||||
color='primary'
|
||||
hide-selected
|
||||
small-chips
|
||||
:items='items'
|
||||
@change='change'
|
||||
hide-no-data
|
||||
@input.native='search'
|
||||
item-text='label'
|
||||
return-object
|
||||
chips
|
||||
multiple)
|
||||
template(v-slot:selection="{ attrs, item }")
|
||||
v-chip(v-bind="attrs"
|
||||
close
|
||||
@click:close='remove(item)'
|
||||
:close-icon='mdiCloseCircle')
|
||||
v-avatar(left)
|
||||
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
115
components/WhereInput.vue
Normal 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>
|
207
components/admin/Cohorts.vue
Normal file
207
components/admin/Cohorts.vue
Normal 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>
|
|
@ -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' }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,19 +1,41 @@
|
|||
---
|
||||
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
|
||||
## Enable registration
|
||||
## Confirm registration
|
||||
## Confirm event
|
||||
## Confirm event
|
|
@ -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 }}">
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
@ -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']
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template lang='pug'>
|
||||
v-app#iframe
|
||||
nuxt
|
||||
v-app#iframe
|
||||
nuxt
|
||||
</template>
|
||||
<style lang='less'>
|
||||
<style>
|
||||
#iframe.v-application {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
|
|
@ -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!"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.",
|
||||
|
@ -151,7 +152,7 @@
|
|||
"each_month": "Each month",
|
||||
"due": "until",
|
||||
"from": "From",
|
||||
"image_too_big": "The image can't be bigger than 4 MB",
|
||||
"image_too_big": "The image can't be bigger than 4MB",
|
||||
"interact_with_me_at": "Interact with me on fediverse at",
|
||||
"follow_me_description": "One of the ways to stay up to date on events published here on {title},\nis following the account <u>{account}</u> from the fediverse, for example via Mastodon, and possibly add resources to an event from there.<br/><br/>\nIf you have never heard of Mastodon and the fediverse we recommend reading <a href='https://www.savjee.be/videos/simply-explained/mastodon-and-fediverse-explained/'>this article</a>.<br/><br/>Enter your instance below (e.g. mastodon.social)",
|
||||
"interact_with_me": "Follow me",
|
||||
|
@ -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!"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,19 +105,19 @@
|
|||
"intro": "A diferencia de las plataformas del capitalismo, que hacen todo lo posible para mantener datos y usuarios dentro de ellas, creemos las informaciones, así como las personas, deben ser libres. Para ello, puedes mantenerte enterado sobre los eventos que te interesan como mejor te parezca, sin necesariamente tener que pasar por este sitio.",
|
||||
"email_description": "Puedes recibir por mail los eventos que te interesan.",
|
||||
"insert_your_address": "Casilla de correo",
|
||||
"feed_description": "Para seguir las actualizaciones desde un ordenador o teléfono inteligente sin la necesidad de abrir periódicamente el sitio, el método recomendado es usar los feeds RSS.</p>\n\n <p>Con rss feeds, utilizas una aplicación especial para recibir actualizaciones de los sitios que más te interesan, como por ejemplo éste. Es una buena manera de seguir muchos sitios muy rápidamente, sin la necesidad de crear una cuenta u otras complicaciones.</p>\n \n <li>Si tienes Android, te sugerimos <a href=\"https://f-droid.org/es/packages/com.nononsenseapps.feeder/\">Feeder</a> o Feeder</li>\n <li>Para iPhone/iPad puedes usar <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\">Feed4U</a></li>\n <li>En el caso de un ordenador aconsejamos Feedbro, se instala como plugin <a href=\"https://addons.mozilla.org/es-ES/firefox/addon/feedbroreader/\">de Firefox </a>o <a href=\"https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa\">de Chrome</a> y funciona con todos los principales sistemas.</li>\n <br/>\n Agregando este link a tu lector de feed, estarás siempre actualizado/a.",
|
||||
"feed_description": "Para seguir las actualizaciones desde un ordenador o teléfono inteligente sin la necesidad de abrir periódicamente el sitio, el método recomendado es usar los feeds RSS.</p>\n\n<p>Con rss feeds, utilizas una aplicación especial para recibir actualizaciones de los sitios que más te interesan, como por ejemplo éste. Es una buena manera de seguir muchos sitios muy rápidamente, sin la necesidad de crear una cuenta u otras complicaciones.</p>\n\n<li>Si tienes Android, te sugerimos <a href=\"https://f-droid.org/es/packages/com.nononsenseapps.feeder/\">Feeder</a> o Feeder</li>\n<li>Para iPhone/iPad puedes usar <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\">Feed4U</a></li>\n<li>En el caso de un ordenador aconsejamos Feedbro, se instala como plugin <a href=\"https://addons.mozilla.org/es-ES/firefox/addon/feedbroreader/\">de Firefox </a>o <a href=\"https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa\">de Chrome</a> y funciona con todos los principales sistemas.</li>\n<br/>\nAgregando este link a tu lector de feed, estarás siempre actualizado/a.",
|
||||
"ical_description": "Las computadoras y los teléfonos inteligentes suelen estar equipados con una aplicación para administrar un calendario. Estos programas generalmente se pueden usar para importar un calendario remoto.",
|
||||
"list_description": "Si tienes un sitio web y quieres mostrar una lista de eventos, puedes usar el siguiente código"
|
||||
},
|
||||
"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>"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 !"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -145,12 +145,16 @@
|
|||
"normal_description": "Elixe o día.",
|
||||
"recurrent_1w_days": "Cada {days}",
|
||||
"recurrent_1m_ordinal": "O {n} {days} de cada mes",
|
||||
"image_too_big": "A imaxe non pode superar os 4 MB",
|
||||
"image_too_big": "A imaxe non pode superar os 4MB",
|
||||
"recurrent_1m_days": "|O {days} de cada mes|{days} de cada mes",
|
||||
"each_2w": "Cada dúas semanas",
|
||||
"follow_me_description": "Un dos xeitos de recibir actualizacións dos eventos que se publican aquí en {title},\né seguindo a conta <u>{account}</u> no fediverso, por exemplo a través de Mastodon, e posiblemente tamén engadir recursos para un evento desde alí.<br/><br/>\nSe nunco escoitaches falar de Mastodon e o fediverso recomendámosche ler <a href='https://www.savjee.be/videos/simply-explained/mastodon-and-fediverse-explained/'>este artigo</a>.<br/><br/>Escribe aquí a túa instancia (ex. mastodon.social)",
|
||||
"ics": "ICS",
|
||||
"import_description": "Podes importar eventos desde outras plataformas e outras instancias usando formatos estándar (ics e h-event)"
|
||||
"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?",
|
||||
|
|
|
@ -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!"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
||||
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: {
|
||||
|
|
60
package.json
60
package.json
|
@ -1,18 +1,19 @@
|
|||
{
|
||||
"name": "gancio",
|
||||
"version": "1.4.3",
|
||||
"version": "1.5.0-rc.2",
|
||||
"description": "A shared agenda for local communities",
|
||||
"author": "lesion",
|
||||
"scripts": {
|
||||
"build": "nuxt build --modern",
|
||||
"start:inspect": "NODE_ENV=production node --inspect node_modules/.bin/nuxt start --modern",
|
||||
"dev": "nuxt dev",
|
||||
"test": "cd tests/seeds; jest; cd ../..",
|
||||
"test": "cd tests/seeds; jest ; cd ../..",
|
||||
"start": "nuxt start --modern",
|
||||
"doc": "cd docs && bundle exec jekyll b",
|
||||
"doc:dev": "cd docs && bundle exec jekyll s --drafts",
|
||||
"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"
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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' })
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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`, '')
|
||||
|
@ -330,11 +331,4 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.main_image {
|
||||
margin: 0 auto;
|
||||
border-radius: 5px;
|
||||
transition: max-height 0.2s;
|
||||
}
|
||||
</style>
|
||||
</script>
|
|
@ -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
31
pages/g/_cohort.vue
Normal 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>
|
111
pages/index.vue
111
pages/index.vue
|
@ -1,43 +1,41 @@
|
|||
<template lang="pug">
|
||||
v-container#home(fluid)
|
||||
v-container#home(fluid)
|
||||
|
||||
//- Announcements
|
||||
#announcements.mx-1.mt-1(v-if='announcements.length')
|
||||
Announcement(v-for='announcement in announcements' :key='`a_${announcement.id}`' :announcement='announcement')
|
||||
//- Announcements
|
||||
#announcements.mx-1.mt-1(v-if='announcements.length')
|
||||
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
|
||||
//- 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')
|
||||
//- Calendar and search bar
|
||||
v-row.ma-2
|
||||
#calh.col-xl-5.col-lg-5.col-md-7.col-sm-12.col-xs-12.pa-0.ma-0
|
||||
//- this is needed as v-calendar does not support SSR
|
||||
//- https://github.com/nathanreyes/v-calendar/issues/336
|
||||
client-only(placeholder='Loading...')
|
||||
Calendar(@dayclick='dayChange' @monthchange='monthChange' :events='events')
|
||||
|
||||
.col.pt-0.pt-md-2
|
||||
Search(:filters='filters' @update='updateFilters')
|
||||
v-chip(v-if='selectedDay' close :close-icon='mdiCloseCircle' @click:close='dayChange()') {{selectedDay}}
|
||||
|
||||
//- Events
|
||||
#events.mb-2.mt-1.pl-1.pl-sm-2
|
||||
Event(:event='event' @destroy='destroy' v-for='(event, idx) in visibleEvents' :lazy='idx>2' :key='event.id' @tagclick='tagClick' @placeclick='placeClick')
|
||||
.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')
|
||||
</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
30
pages/p/_place.vue
Normal 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
42
pages/search.vue
Normal 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>
|
|
@ -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>
|
|
@ -1,32 +1,26 @@
|
|||
<template lang="pug">
|
||||
v-container.pa-6
|
||||
h2.mb-2.text-center Gancio Setup
|
||||
v-stepper.grey.lighten-5(v-model='step')
|
||||
v-stepper-header
|
||||
v-stepper-step(v-show='!dbdone' :complete='step > 1' step='1') Database
|
||||
v-divider(v-show='!dbdone')
|
||||
v-stepper-step(:complete='step > 2' step='2') Configuration
|
||||
v-divider
|
||||
v-stepper-step(:complete='step > 3' step='3') Finish
|
||||
|
||||
v-container.pa-6
|
||||
h2.mb-2.text-center Gancio Setup
|
||||
v-stepper.grey.lighten-5(v-model='step')
|
||||
v-stepper-header
|
||||
v-stepper-step(v-show='!dbdone' :complete='step > 1' step='1') Database
|
||||
v-divider(v-show='!dbdone')
|
||||
v-stepper-step(:complete='step > 2' step='2') Configuration
|
||||
v-divider
|
||||
v-stepper-step(:complete='step > 3' step='3') Finish
|
||||
|
||||
v-stepper-items
|
||||
v-stepper-content(v-show='!dbdone' step='1')
|
||||
DbStep(@complete='dbCompleted')
|
||||
v-stepper-content(step='2')
|
||||
Settings(setup, @complete='configCompleted')
|
||||
v-stepper-content(step='3')
|
||||
Completed(ref='completed')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
v-stepper-items
|
||||
v-stepper-content(v-show='!dbdone' step='1')
|
||||
DbStep(@complete='dbCompleted')
|
||||
v-stepper-content(step='2')
|
||||
Settings(setup, @complete='configCompleted')
|
||||
v-stepper-content(step='3')
|
||||
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
30
pages/tag/_tag.vue
Normal 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>
|
|
@ -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))
|
||||
|
|
|
@ -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')}`
|
||||
})
|
||||
}
|
||||
|
|
191
server/api/controller/cohort.js
Normal file
191
server/api/controller/cohort.js
Normal 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
|
|
@ -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) {
|
||||
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 {
|
||||
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)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if (url && !event.recurrent) {
|
||||
// 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)
|
||||
}
|
||||
|
||||
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) {
|
||||
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 {
|
||||
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)
|
||||
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 []
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
59
server/api/controller/place.js
Normal file
59
server/api/controller/place.js
Normal 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))
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
47
server/api/controller/tag.js
Normal file
47
server/api/controller/tag.js
Normal 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: }
|
||||
// ]
|
||||
// }})
|
||||
// }
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
27
server/api/models/cohort.js
Normal file
27
server/api/models/cohort.js
Normal 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
|
|
@ -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
|
||||
|
|
24
server/api/models/filter.js
Normal file
24
server/api/models/filter.js
Normal 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
|
|
@ -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') {
|
||||
|
|
|
@ -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', () => {
|
||||
cb(null, {
|
||||
destination: config.upload_path,
|
||||
filename,
|
||||
path: finalPath,
|
||||
size: outStream.bytesWritten
|
||||
file.stream.pipe(sharpStream)
|
||||
Promise.all(promises)
|
||||
.then(res => {
|
||||
const info = res[1]
|
||||
cb(null, {
|
||||
destination: config.upload_path,
|
||||
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)
|
||||
|
|
|
@ -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 => {
|
||||
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)
|
||||
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(['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
|
||||
|
|
|
@ -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
|
||||
module.exports = accountsCLI
|
|
@ -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'),
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// needed by sequelize
|
||||
const config = require('./config')
|
||||
config.load()
|
||||
module.exports = config.db
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}"`)
|
||||
|
|
|
@ -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,41 +121,34 @@ 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) {
|
||||
|
|
|
@ -1,14 +1,29 @@
|
|||
const config = require('../server/config')
|
||||
config.load()
|
||||
|
||||
module.exports = function () {
|
||||
const config = require('../server/config')
|
||||
config.load()
|
||||
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) {
|
||||
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')
|
||||
dayjs.extend(timezone)
|
||||
if (config.status == 'READY') {
|
||||
await db.initialize()
|
||||
} else {
|
||||
|
@ -18,41 +33,33 @@ 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
|
||||
if (config.status === 'READY' && process.env.NODE_ENV == 'production') {
|
||||
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', shutdown)
|
||||
process.on('SIGINT', shutdown)
|
||||
|
||||
process.on('SIGTERM', initialize.shutdown)
|
||||
process.on('SIGINT', initialize.shutdown)
|
||||
}
|
||||
|
||||
return start(this.nuxt)
|
||||
}
|
||||
|
||||
|
||||
module.exports = initialize
|
30
server/migrations/20220512195507-cohort.js
Normal file
30
server/migrations/20220512195507-cohort.js
Normal 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')
|
||||
}
|
||||
};
|
35
server/migrations/20220512195953-filter.js
Normal file
35
server/migrations/20220512195953-filter.js
Normal 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')
|
||||
}
|
||||
|
||||
}
|
|
@ -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,13 +37,14 @@ 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
|
||||
// ignore unimplemented ping url from fediverse
|
||||
app.use(spamFilter)
|
||||
|
||||
// fill res.locals.user if request is authenticated
|
||||
|
@ -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')
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue