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)
|
||||
export function attributesFromEvents (_events) {
|
||||
|
||||
// const colors = ['teal', 'green', 'yellow', 'teal', 'indigo', 'green', 'red', 'purple', 'pink', 'gray']
|
||||
// merge events with same date
|
||||
let attributes = []
|
||||
attributes.push({ key: 'today', dates: new Date(), bar: { color: 'green', fillMode: 'outline' } })
|
||||
const now = dayjs().unix()
|
||||
for(let e of _events) {
|
||||
const key = dayjs.unix(e.start_datetime).format('YYYYMMDD')
|
||||
const c = e.start_datetime < now ? 'vc-past' : ''
|
||||
|
||||
function getColor (event, where) {
|
||||
const color = { class: 'vc-rounded-full', color: 'blue', fillMode: where === 'base' ? 'light' : 'solid' }
|
||||
const tag = get(event, 'tags[0]')
|
||||
if (event.start_datetime < now) {
|
||||
if (event.multidate) {
|
||||
color.fillMode = where === 'base' ? 'light' : 'outline'
|
||||
if (where === 'base') {
|
||||
color.class += ' vc-past'
|
||||
const i = attributes.find(a => a.day === key)
|
||||
if (!i) {
|
||||
attributes.push({ day: key, key: e.id, n: 1, dates: new Date(e.start_datetime * 1000),
|
||||
dot: { color: 'teal', class: c } })
|
||||
continue
|
||||
}
|
||||
|
||||
i.n++
|
||||
if (i.n >= 20 ) {
|
||||
i.dot = { color: 'purple', class: c }
|
||||
} else if ( i.n >= 10 ) {
|
||||
i.dot = { color: 'red', class: c}
|
||||
} else if ( i.n >= 5 ) {
|
||||
i.dot = { color: 'orange', class: c}
|
||||
} else if ( i.n >= 3 ) {
|
||||
i.dot = { color: 'yellow', class: c}
|
||||
} else {
|
||||
color.class += ' vc-past'
|
||||
}
|
||||
}
|
||||
if (!tag) { return color }
|
||||
const idx = tags.indexOf(tag)
|
||||
if (idx < 0) { return color }
|
||||
color.color = colors[idx]
|
||||
// if (event.start_datetime < now) { color.class += ' vc-past' }
|
||||
return color
|
||||
i.dot = { color: 'teal', class: c }
|
||||
}
|
||||
|
||||
attributes = attributes.concat(_events
|
||||
.filter(e => !e.multidate)
|
||||
.map(e => {
|
||||
return {
|
||||
key: e.id,
|
||||
dot: getColor(e),
|
||||
dates: new Date(e.start_datetime * 1000)
|
||||
}
|
||||
}))
|
||||
|
||||
attributes = attributes.concat(_events
|
||||
.filter(e => e.multidate)
|
||||
.map(e => ({
|
||||
key: e.id,
|
||||
highlight: {
|
||||
start: getColor(e),
|
||||
base: getColor(e, 'base'),
|
||||
end: getColor(e)
|
||||
},
|
||||
dates: { start: new Date(e.start_datetime * 1000), end: new Date(e.end_datetime * 1000) }
|
||||
})))
|
||||
// add a bar to highlight today
|
||||
attributes.push({ key: 'today', dates: new Date(), highlight: { color: 'green', fillMode: 'outline' } })
|
||||
|
||||
return attributes
|
||||
}
|
||||
|
|
|
@ -31,7 +31,8 @@ li {
|
|||
}
|
||||
|
||||
#calh {
|
||||
height: 292px;
|
||||
/* this is to avoid content shift layout as v-calendar does not support SSR */
|
||||
height: 268px;
|
||||
}
|
||||
|
||||
.container {
|
||||
|
@ -55,7 +56,6 @@ li {
|
|||
scrollbar-color: #FF4511 #111;
|
||||
}
|
||||
|
||||
// EVENT
|
||||
.event {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
@ -67,8 +67,9 @@ li {
|
|||
margin-right: .4em;
|
||||
transition: all .5s;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.title {
|
||||
.event .title {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
margin: 0.5rem 1rem 0.5rem 1rem;
|
||||
|
@ -77,33 +78,22 @@ li {
|
|||
-webkit-box-orient: vertical;
|
||||
font-size: 1.1em !important;
|
||||
line-height: 1.2em !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.body {
|
||||
.event .body {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.img {
|
||||
width: 100%;
|
||||
max-height: 250px;
|
||||
min-height: 160px;
|
||||
object-fit: cover;
|
||||
object-position: top;
|
||||
aspect-ratio: 1.7778;
|
||||
}
|
||||
|
||||
.place {
|
||||
max-width: 100%;
|
||||
span {
|
||||
.event .place span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
a {
|
||||
|
||||
.event a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.vc-past {
|
||||
opacity: 0.4;
|
|
@ -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
|
||||
v-menu(
|
||||
v-model="menuFromHour"
|
||||
:close-on-content-click="false"
|
||||
offset-y
|
||||
:value="fromHour"
|
||||
transition="scale-transition")
|
||||
template(v-slot:activator="{ on, attrs }")
|
||||
v-text-field(
|
||||
:label="$t('event.from')"
|
||||
:value="fromHour"
|
||||
:disabled='!value.from'
|
||||
:prepend-icon="mdiClockTimeFourOutline"
|
||||
:rules="[$validators.required('event.from')]"
|
||||
:items='hourList' @change='hr => change("fromHour", hr)')
|
||||
readonly
|
||||
v-bind="attrs"
|
||||
v-on="on")
|
||||
v-time-picker(
|
||||
v-if="menuFromHour"
|
||||
:value="fromHour"
|
||||
:allowedMinutes='allowedMinutes'
|
||||
format='24hr'
|
||||
@click:minute='menuFromHour=false'
|
||||
@change='hr => change("fromHour", hr)')
|
||||
|
||||
|
||||
v-col.col-12.col-sm-6
|
||||
v-select(dense :label="$t('event.due')"
|
||||
v-menu(
|
||||
v-model="menuDueHour"
|
||||
:close-on-content-click="false"
|
||||
offset-y
|
||||
:value="dueHour"
|
||||
transition="scale-transition")
|
||||
template(v-slot:activator="{ on, attrs }")
|
||||
v-text-field(
|
||||
:label="$t('event.due')"
|
||||
:value="dueHour"
|
||||
:disabled='!fromHour'
|
||||
:value='dueHour' clearable
|
||||
:items='hourList' @change='hr => change("dueHour", hr)')
|
||||
:prepend-icon="mdiClockTimeEightOutline"
|
||||
readonly
|
||||
v-bind="attrs"
|
||||
v-on="on")
|
||||
v-time-picker(
|
||||
v-if="menuDueHour"
|
||||
:value="dueHour"
|
||||
:allowedMinutes='allowedMinutes'
|
||||
format='24hr'
|
||||
@click:minute='menuDueHour=false'
|
||||
@change='hr => change("dueHour", hr)')
|
||||
|
||||
List(v-if='type==="normal" && todayEvents.length' :events='todayEvents' :title='$t("event.same_day")')
|
||||
|
||||
|
@ -52,7 +90,8 @@ v-col(cols=12)
|
|||
import dayjs from 'dayjs'
|
||||
import { mapState } from 'vuex'
|
||||
import List from '@/components/List'
|
||||
import { attributesFromEvents } from '../../assets/helper'
|
||||
import { attributesFromEvents } from '../assets/helper'
|
||||
import { mdiClockTimeFourOutline, mdiClockTimeEightOutline } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
name: 'DateInput',
|
||||
|
@ -63,6 +102,10 @@ export default {
|
|||
},
|
||||
data () {
|
||||
return {
|
||||
mdiClockTimeFourOutline, mdiClockTimeEightOutline,
|
||||
allowedMinutes: [0, 15, 30, 45],
|
||||
menuFromHour: false,
|
||||
menuDueHour: false,
|
||||
type: 'normal',
|
||||
page: null,
|
||||
events: [],
|
||||
|
@ -74,15 +117,14 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['settings', 'tags']),
|
||||
...mapState(['settings']),
|
||||
todayEvents () {
|
||||
const start = dayjs(this.value.from).startOf('day').unix()
|
||||
const end = dayjs(this.value.from).endOf('day').unix()
|
||||
const events = this.events.filter(e => e.start_datetime >= start && e.start_datetime <= end)
|
||||
return events
|
||||
return this.events.filter(e => e.start_datetime >= start && e.start_datetime <= end)
|
||||
},
|
||||
attributes () {
|
||||
return attributesFromEvents(this.events, this.tags)
|
||||
return attributesFromEvents(this.events)
|
||||
},
|
||||
fromDate () {
|
||||
if (this.value.multidate) {
|
||||
|
@ -92,21 +134,10 @@ export default {
|
|||
},
|
||||
|
||||
fromHour () {
|
||||
return this.value.from && this.value.fromHour ? dayjs(this.value.from).format('HH:mm') : null
|
||||
return this.value.from && this.value.fromHour ? dayjs.tz(this.value.from).format('HH:mm') : null
|
||||
},
|
||||
dueHour () {
|
||||
return this.value.due && this.value.dueHour ? dayjs(this.value.due).format('HH:mm') : null
|
||||
},
|
||||
hourList () {
|
||||
const hourList = []
|
||||
const leftPad = h => ('00' + h).slice(-2)
|
||||
for (let h = 0; h < 24; h++) {
|
||||
const textHour = leftPad(h < 13 ? h : h - 12)
|
||||
hourList.push({ text: textHour + ':00 ' + (h <= 12 ? 'AM' : 'PM'), value: leftPad(h) + ':00' })
|
||||
hourList.push({ text: textHour + ':30 ' + (h <= 12 ? 'AM' : 'PM'), value: leftPad(h) + ':30' })
|
||||
}
|
||||
|
||||
return hourList
|
||||
return this.value.due && this.value.dueHour ? dayjs.tz(this.value.due).format('HH:mm') : null
|
||||
},
|
||||
whenPatterns () {
|
||||
if (!this.value.from) { return }
|
||||
|
@ -196,7 +227,7 @@ export default {
|
|||
} else if (what === 'fromHour') {
|
||||
if (value) {
|
||||
const [hour, minute] = value.split(':')
|
||||
const from = dayjs(this.value.from).hour(hour).minute(minute).second(0)
|
||||
const from = dayjs.tz(this.value.from).hour(hour).minute(minute).second(0)
|
||||
this.$emit('input', { ...this.value, from, fromHour: true })
|
||||
} else {
|
||||
this.$emit('input', { ...this.value, fromHour: false })
|
||||
|
@ -204,7 +235,7 @@ export default {
|
|||
} else if (what === 'dueHour') {
|
||||
if (value) {
|
||||
const [hour, minute] = value.split(':')
|
||||
const fromHour = dayjs(this.value.from).hour()
|
||||
const fromHour = dayjs.tz(this.value.from).hour()
|
||||
|
||||
// add a day
|
||||
let due = dayjs(this.value.from)
|
||||
|
@ -226,20 +257,20 @@ export default {
|
|||
let from = value.start
|
||||
let due = value.end
|
||||
if (this.value.fromHour) {
|
||||
from = dayjs(value.start).hour(dayjs(this.value.from).hour())
|
||||
from = dayjs.tz(value.start).hour(dayjs.tz(this.value.from).hour())
|
||||
}
|
||||
if (this.value.dueHour) {
|
||||
due = dayjs(value.end).hour(dayjs(this.value.due).hour())
|
||||
due = dayjs.tz(value.end).hour(dayjs.tz(this.value.due).hour())
|
||||
}
|
||||
this.$emit('input', { ...this.value, from, due })
|
||||
} else {
|
||||
let from = value
|
||||
let due = this.value.due
|
||||
if (this.value.fromHour) {
|
||||
from = dayjs(value).hour(dayjs(this.value.from).hour())
|
||||
from = dayjs.tz(value).hour(dayjs.tz(this.value.from).hour())
|
||||
}
|
||||
if (this.value.dueHour && this.value.due) {
|
||||
due = dayjs(value).hour(dayjs(this.value.due).hour())
|
||||
due = dayjs.tz(value).hour(dayjs.tz(this.value.due).hour())
|
||||
}
|
||||
this.$emit('input', { ...this.value, from, due })
|
||||
}
|
|
@ -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'>
|
||||
<style>
|
||||
#list {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
.v-list-item__title {
|
||||
}
|
||||
#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-if='settings.allow_recurrent_event'
|
||||
v-model='showRecurrent'
|
||||
inset color='primary'
|
||||
hide-details
|
||||
:label="$t('event.show_recurrent')")
|
||||
v-autocomplete(
|
||||
v-model='meta'
|
||||
:label='$t("common.search")'
|
||||
:items='keywords'
|
||||
:filter='filter'
|
||||
cache-items
|
||||
hide-details
|
||||
color='primary'
|
||||
hide-selected
|
||||
small-chips
|
||||
:items='items'
|
||||
@change='change'
|
||||
:value='selectedFilters'
|
||||
clearable
|
||||
:search-input.sync='search'
|
||||
hide-no-data
|
||||
@input.native='search'
|
||||
item-text='label'
|
||||
return-object
|
||||
chips single-line
|
||||
chips
|
||||
multiple)
|
||||
template(v-slot:selection="data")
|
||||
v-chip(v-bind="data.attrs"
|
||||
template(v-slot:selection="{ attrs, item }")
|
||||
v-chip(v-bind="attrs"
|
||||
close
|
||||
:close-icon='mdiCloseCircle'
|
||||
@click:close='remove(data.item)'
|
||||
:input-value="data.selected")
|
||||
@click:close='remove(item)'
|
||||
:close-icon='mdiCloseCircle')
|
||||
v-avatar(left)
|
||||
v-icon(v-text="data.item.type === 'place' ? mdiMapMarker : mdiTag")
|
||||
span {{ data.item.label }}
|
||||
v-icon(v-text="item.type === 'place' ? mdiMapMarker : mdiTag")
|
||||
span {{ item.label }}
|
||||
template(v-slot:item='{ item }')
|
||||
v-list-item-avatar
|
||||
v-icon(v-text="item.type === 'place' ? mdiMapMarker : mdiTag")
|
||||
v-list-item-content
|
||||
v-list-item-title(v-text='item.label')
|
||||
v-list-item-subtitle(v-if='item.type ==="place"' v-text='item.address')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { mdiMapMarker, mdiTag, mdiCloseCircle } from '@mdi/js'
|
||||
import debounce from 'lodash/debounce'
|
||||
|
||||
export default {
|
||||
name: 'Search',
|
||||
props: {
|
||||
recurrentFilter: { type: Boolean, default: true },
|
||||
filters: { type: Object, default: () => {} }
|
||||
filters: { type: Object, default: () => ({}) }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
mdiTag, mdiMapMarker, mdiCloseCircle,
|
||||
tmpfilter: null,
|
||||
search: ''
|
||||
meta: [],
|
||||
items: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['tags', 'places', 'settings']),
|
||||
...mapState(['settings']),
|
||||
showRecurrent: {
|
||||
get () {
|
||||
return this.filters.show_recurrent
|
||||
},
|
||||
set (v) {
|
||||
const filters = {
|
||||
tags: this.filters.tags,
|
||||
places: this.filters.places,
|
||||
show_recurrent: v
|
||||
}
|
||||
this.$emit('update', filters)
|
||||
this.change(v)
|
||||
}
|
||||
},
|
||||
selectedFilters () {
|
||||
const tags = this.tags.filter(t => this.filters.tags.includes(t.tag)).map(t => ({ type: 'tag', label: t.tag, weigth: t.weigth, id: t.tag }))
|
||||
const places = this.places.filter(p => this.filters.places.includes(p.id))
|
||||
.map(p => ({ type: 'place', label: p.name, weigth: p.weigth, id: p.id }))
|
||||
const keywords = tags.concat(places).sort((a, b) => b.weigth - a.weigth)
|
||||
return keywords
|
||||
},
|
||||
keywords () {
|
||||
const tags = this.tags.map(t => ({ type: 'tag', label: t.tag, weigth: t.weigth, id: t.tag }))
|
||||
const places = this.places.map(p => ({ type: 'place', label: p.name, weigth: p.weigth, id: p.id }))
|
||||
const keywords = tags.concat(places).sort((a, b) => b.weigth - a.weigth)
|
||||
return keywords
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
remove (item) {
|
||||
const filters = {
|
||||
tags: item.type === 'tag' ? this.filters.tags.filter(f => f !== item.id) : this.filters.tags,
|
||||
places: item.type === 'place' ? this.filters.places.filter(f => f !== item.id) : this.filters.places,
|
||||
show_recurrent: this.filters.show_recurrent
|
||||
}
|
||||
this.$emit('update', filters)
|
||||
filter (item, queryText, itemText) {
|
||||
return itemText.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1 ||
|
||||
item.address && item.address.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1
|
||||
},
|
||||
change (filters) {
|
||||
filters = {
|
||||
tags: filters.filter(t => t.type === 'tag').map(t => t.id),
|
||||
places: filters.filter(p => p.type === 'place').map(p => p.id),
|
||||
show_recurrent: this.filters.show_recurrent
|
||||
search: debounce(async function(search) {
|
||||
this.items = await this.$axios.$get(`/event/meta?search=${search.target.value}`)
|
||||
}, 100),
|
||||
remove (item) {
|
||||
this.meta = this.meta.filter(m => m.type !== item.type || m.type === 'place' ? m.id !== item.id : m.tag !== item.tag)
|
||||
this.change()
|
||||
},
|
||||
change (show_recurrent) {
|
||||
const filters = {
|
||||
tags: this.meta.filter(t => t.type === 'tag').map(t => t.label),
|
||||
places: this.meta.filter(p => p.type === 'place').map(p => p.id),
|
||||
show_recurrent: typeof show_recurrent !== 'undefined' ? show_recurrent : this.filters.show_recurrent
|
||||
}
|
||||
this.$emit('update', filters)
|
||||
}
|
||||
|
|
115
components/WhereInput.vue
Normal file
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,16 +1,38 @@
|
|||
---
|
||||
layout: default
|
||||
title: Admin
|
||||
permalink: /admin
|
||||
nav_order: 5
|
||||
permalink: /usage/admin
|
||||
nav_order: 1
|
||||
parent: Usage
|
||||
---
|
||||
|
||||
|
||||
# Admin
|
||||
{: .no_toc }
|
||||
### CLI
|
||||
|
||||
#### Manage accounts
|
||||
|
||||
|
||||
```bash
|
||||
$ gancio accounts
|
||||
|
||||
Manage accounts
|
||||
|
||||
Commands:
|
||||
gancio accounts list List all accounts
|
||||
|
||||
```
|
||||
|
||||
#### List accounts
|
||||
|
||||
```
|
||||
$ gancio accounts list
|
||||
📅 gancio - v1.4.3 - A shared agenda for local communities (nodejs: v16.13.0)
|
||||
> Reading configuration from: ./config.json
|
||||
|
||||
1 admin: true enabled: true email: admin
|
||||
2 admin: false enabled: true email: lesion@autistici.org
|
||||
```
|
||||
|
||||
1. TOC
|
||||
{:toc}
|
||||
|
||||
## Basics
|
||||
## Add user
|
||||
|
|
|
@ -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']
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
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.",
|
||||
|
@ -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!"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -110,14 +110,14 @@
|
|||
"list_description": "Si tienes un sitio web y quieres mostrar una lista de eventos, puedes usar el siguiente código"
|
||||
},
|
||||
"register": {
|
||||
"description": "Los movimientos sociales necesitan organizarse y autofinanciarse. <br/> Este es un regalo para ustedes, úsenlo solamente para eventos con fines no comerciales y obviamente antifascistas, antisexistas y antirracistas.\n<br/> Antes de que puedas publicar <strong> debemos aprobar la cuenta </strong>. Como imaginarás, <strong> detrás de este sitio hay personas </strong> de carne y hueso, por esto te pedimos escribir algo para hacernos saber que tipos de eventos te gustaría publicar.",
|
||||
"description": "Los movimientos sociales necesitan organizarse y autofinanciarse. <br/>\nEste es un regalo para ustedes, úsenlo solamente para eventos con fines no comerciales y obviamente antifascistas, antisexistas y antirracistas.\n<br/> Antes de que puedas publicar, <strong> debemos aprobar la cuenta </strong>. Como imaginarás, <strong> detrás de este sitio hay personas de carne y hueso</strong>, por esto te pedimos escribir algo para hacernos saber que tipos de eventos te gustaría publicar.",
|
||||
"error": "Error: ",
|
||||
"complete": "Confirmaremos el registro lo antes posible.",
|
||||
"first_user": "Administrador creado y activado"
|
||||
},
|
||||
"event": {
|
||||
"anon": "Anónimo",
|
||||
"anon_description": "Puedes ingresar un evento sin registrarte o iniciar sesión, pero en este caso tendrás que esperar a que alguien lo lea para confirmar que es un evento adecuado para este espacio,\ndelegando esta elección. Además, no será posible modificarlo. <br/> <br/>\nSi no te gusta, puedes <a href='/login'> iniciar sesión </a> o <a href='/register'> registrarte </a>,\nde lo contrario, continúa y recibirás una respuesta lo antes posible. ",
|
||||
"anon_description": "Puedes ingresar un evento sin registrarte o iniciar sesión, pero en este caso tendrás que esperar a que alguien lo lea para confirmar que es un evento adecuado para este espacio,\ndelegando esta elección. Además, no será posible modificarlo. <br/> <br/>\nSi no te gusta, puedes <a href='/login'> iniciar sesión </a> o <a href='/register'> registrarte </a>. De lo contrario, continúa y recibirás una respuesta lo antes posible. ",
|
||||
"same_day": "Mismo día",
|
||||
"what_description": "Nombre evento",
|
||||
"description_description": "Descripción, puedes copiar y pegar",
|
||||
|
@ -148,7 +148,7 @@
|
|||
"from": "Desde las",
|
||||
"image_too_big": "La imagén es demasiado grande! Tamaño máx 4M",
|
||||
"interact_with_me_at": "Sígueme en el fediverso en",
|
||||
"show_recurrent": "Eventos recurrientes",
|
||||
"show_recurrent": "Eventos recurrentes",
|
||||
"show_past": "eventos pasados",
|
||||
"follow_me_description": "Entre las diversas formas de mantenerse al día con los eventos publicados aquí en {title},\npuedes seguir la cuenta <u>{account}</u> desde el fediverso, por ejemplo, a través de un Mastodon, y posiblemente añadir recursos a un evento desde allí.<br/><br/>\nSi nunca has oído hablar del Mastodon y el fediverso te sugerimos que leas <a href='https://cagizero.wordpress.com/2018/10/25/cose-mastodon/'>este artículo</a>.<br/><br/> Introduce tu instancia abajo (por ejemplo mastodon.cisti.org o mastodon.bida.im)",
|
||||
"interact_with_me": "Sigueme en el fediverso",
|
||||
|
@ -221,7 +221,14 @@
|
|||
"is_dark": "Tema oscuro",
|
||||
"instance_block_confirm": "¿Estás seguro/a que quieres bloquear la instancia {instance}?",
|
||||
"add_instance": "Añadir instancia",
|
||||
"disable_user_confirm": "Estas seguro de que quieres deshabilitar a {user}?"
|
||||
"disable_user_confirm": "Estas seguro de que quieres deshabilitar a {user}?",
|
||||
"widget": "Widget",
|
||||
"show_smtp_setup": "Ajustes de correo",
|
||||
"smtp_hostname": "Nombre del equipo SMTP",
|
||||
"smtp_test_success": "Un correo de prueba se ha enviado a {admin_email}. Por favos, comprueba tu bandeja de entrada",
|
||||
"smtp_test_button": "Enviar correo de prueba",
|
||||
"admin_email": "Correo del administrador",
|
||||
"smtp_description": "<ul><li>El administrador debería recibir un correo cuando son añadidos eventos anónimos (si está habilitado).</li><li>El administrador debería recibir un correo de petición de registro (si está habilitado).</li><li>El usuario debería recibir un correo de petición de registro.</li><li>El usuario debería recibir un correo de confirmación de registro.</li><li>El usuario debería recibir un correo de confirmación cuando el administrador le subscriba directamente.</li><li>El usuario debería recibir un correo para restaurar la contraseña cuando la haya olvidado.</li></ul>"
|
||||
},
|
||||
"auth": {
|
||||
"not_confirmed": "Todavía no hemos confirmado este email…",
|
||||
|
@ -263,5 +270,10 @@
|
|||
"validators": {
|
||||
"email": "Introduce un correo electrónico valido",
|
||||
"required": "{fieldName} es requerido"
|
||||
},
|
||||
"setup": {
|
||||
"completed": "Configuración completada",
|
||||
"start": "Inicio",
|
||||
"completed_description": "<p>Puedes ingresar con el siguiente usuario:<br/><br/>Usuario: <b>{email}</b><br/>Contraseña: <b>{password}<b/></p>"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 !"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -150,7 +150,11 @@
|
|||
"each_2w": "Cada dúas semanas",
|
||||
"follow_me_description": "Un dos xeitos de recibir actualizacións dos eventos que se publican aquí en {title},\né seguindo a conta <u>{account}</u> no fediverso, por exemplo a través de Mastodon, e posiblemente tamén engadir recursos para un evento desde alí.<br/><br/>\nSe nunco escoitaches falar de Mastodon e o fediverso recomendámosche ler <a href='https://www.savjee.be/videos/simply-explained/mastodon-and-fediverse-explained/'>este artigo</a>.<br/><br/>Escribe aquí a túa instancia (ex. mastodon.social)",
|
||||
"ics": "ICS",
|
||||
"import_description": "Podes importar eventos desde outras plataformas e outras instancias usando formatos estándar (ics e h-event)"
|
||||
"import_description": "Podes importar eventos desde outras plataformas e outras instancias usando formatos estándar (ics e h-event)",
|
||||
"alt_text_description": "Descrición para persoas con problemas de visión",
|
||||
"choose_focal_point": "Elixe onde centrar a atención",
|
||||
"remove_media_confirmation": "Confirmas a eliminación da imaxe?",
|
||||
"download_flyer": "Descargar folleto"
|
||||
},
|
||||
"admin": {
|
||||
"place_description": "Se escribiches mal o lugar ou enderezo, podes cambialo.<br/>Cambiará o enderezo de tódolos eventos actuais e pasados asociados a este lugar.",
|
||||
|
@ -217,7 +221,9 @@
|
|||
"show_smtp_setup": "Axustes do email",
|
||||
"smtp_hostname": "Servidor SMTP",
|
||||
"new_announcement": "Novo anuncio",
|
||||
"smtp_description": "<ul><li>Admin debería recibir un email cando se engade un evento anónimo (se está activo)</li><li>Admin debería recibir un email coas solicitudes de rexistro (se activo).</li><li>A usuaria debería recibir un email coa solicitude de rexistro.</li><li>A usuaria debería recibir un email confirmando o rexistro.</li><li>A usuaria debería recibir un email de confirmación cando fose subscrita directamente por Admin</li><li>As usuarias deberían recibir un email para restablecer o contrasinal se o esquecen</li></ul>"
|
||||
"smtp_description": "<ul><li>Admin debería recibir un email cando se engade un evento anónimo (se está activo)</li><li>Admin debería recibir un email coas solicitudes de rexistro (se activo).</li><li>A usuaria debería recibir un email coa solicitude de rexistro.</li><li>A usuaria debería recibir un email confirmando o rexistro.</li><li>A usuaria debería recibir un email de confirmación cando fose subscrita directamente por Admin</li><li>As usuarias deberían recibir un email para restablecer o contrasinal se o esquecen</li></ul>",
|
||||
"wrong_domain_warning": "O url base configurado en config.json <b>({baseurl)</b> é diferente ao que estás a visitar <b>({url})</b>",
|
||||
"event_remove_ok": "Evento eliminado"
|
||||
},
|
||||
"auth": {
|
||||
"not_confirmed": "Aínda non foi confirmado…",
|
||||
|
@ -263,7 +269,9 @@
|
|||
"setup": {
|
||||
"completed": "Configuración completada",
|
||||
"completed_description": "<p>Xa podes acceder con estas credenciais:<br/><br/>Identificador: <b>{email}</b><br/>Contrasinal: <b>{password}</b></p>",
|
||||
"start": "Comezar"
|
||||
"start": "Comezar",
|
||||
"https_warning": "Estás entrando con HTTP, lembra cambiar baseurl no config.json se cambias a HTTPS!",
|
||||
"copy_password_dialog": "Si, tes que copiar o contrasinal!"
|
||||
},
|
||||
"login": {
|
||||
"forgot_password": "Esqueceches o contrasinal?",
|
||||
|
|
|
@ -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',
|
||||
|
||||
/*
|
||||
** 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: {
|
||||
|
|
58
package.json
58
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "gancio",
|
||||
"version": "1.4.3",
|
||||
"version": "1.5.0-rc.2",
|
||||
"description": "A shared agenda for local communities",
|
||||
"author": "lesion",
|
||||
"scripts": {
|
||||
|
@ -12,7 +12,8 @@
|
|||
"doc": "cd docs && bundle exec jekyll b",
|
||||
"doc:dev": "cd docs && bundle exec jekyll s --drafts",
|
||||
"migrate": "NODE_ENV=production sequelize db:migrate",
|
||||
"migrate:dev": "sequelize db:migrate"
|
||||
"migrate:dev": "sequelize db:migrate",
|
||||
"build:wc": "cd webcomponents; yarn build:lib; cp dist/gancio-events.es.js ../wp-plugin/js/; cp dist/gancio-events.es.js ../assets/; cp dist/gancio-events.es.js ../docs/assets/js/"
|
||||
},
|
||||
"files": [
|
||||
"server/",
|
||||
|
@ -27,19 +28,20 @@
|
|||
"yarn.lock"
|
||||
],
|
||||
"dependencies": {
|
||||
"@mdi/js": "^6.5.95",
|
||||
"@mdi/js": "^6.7.96",
|
||||
"@nuxtjs/auth": "^4.9.1",
|
||||
"@nuxtjs/axios": "^5.13.5",
|
||||
"@nuxtjs/sitemap": "^2.4.0",
|
||||
"accept-language": "^3.0.18",
|
||||
"axios": "^0.26.0",
|
||||
"axios": "^0.27.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.19.2",
|
||||
"body-parser": "^1.20.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"dayjs": "^1.10.7",
|
||||
"dompurify": "^2.3.6",
|
||||
"dayjs": "^1.11.3",
|
||||
"dompurify": "^2.3.8",
|
||||
"email-templates": "^8.0.9",
|
||||
"express": "^4.17.3",
|
||||
"express": "^4.18.1",
|
||||
"express-oauth-server": "lesion/express-oauth-server#master",
|
||||
"http-signature": "^1.3.6",
|
||||
"ical.js": "^1.5.0",
|
||||
|
@ -49,55 +51,41 @@
|
|||
"linkify-html": "^3.0.4",
|
||||
"linkifyjs": "3.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"mariadb": "^2.5.6",
|
||||
"mariadb": "^3.0.0",
|
||||
"microformat-node": "^2.0.1",
|
||||
"minify-css-string": "^1.0.0",
|
||||
"mkdirp": "^1.0.4",
|
||||
"multer": "^1.4.3",
|
||||
"nuxt-edge": "^2.16.0-27305297.ab1c6cb4",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nuxt-edge": "^2.16.0-27358576.777a4b7f",
|
||||
"pg": "^8.6.0",
|
||||
"sequelize": "^6.17.0",
|
||||
"sequelize-slugify": "^1.6.0",
|
||||
"sequelize": "^6.20.1",
|
||||
"sequelize-slugify": "^1.6.1",
|
||||
"sharp": "^0.27.2",
|
||||
"sqlite3": "mapbox/node-sqlite3#918052b",
|
||||
"sqlite3": "^5.0.8",
|
||||
"tiptap": "^1.32.0",
|
||||
"tiptap-extensions": "^1.35.0",
|
||||
"umzug": "^2.3.0",
|
||||
"v-calendar": "2.4.1",
|
||||
"v-calendar": "^2.4.1",
|
||||
"vue": "^2.6.14",
|
||||
"vue-i18n": "^8.26.7",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"vuetify": "npm:@vuetify/nightly@dev",
|
||||
"winston": "^3.6.0",
|
||||
"winston-daily-rotate-file": "^4.6.1",
|
||||
"yargs": "^17.2.0"
|
||||
"winston": "^3.7.2",
|
||||
"winston-daily-rotate-file": "^4.7.1",
|
||||
"yargs": "^17.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxtjs/vuetify": "^1.12.3",
|
||||
"jest": "^27.5.1",
|
||||
"less": "^4.1.1",
|
||||
"less-loader": "^7",
|
||||
"prettier": "^2.3.0",
|
||||
"jest": "^28.1.0",
|
||||
"prettier": "^2.6.2",
|
||||
"pug": "^3.0.2",
|
||||
"pug-plain-loader": "^1.1.0",
|
||||
"sass": "^1.49.4",
|
||||
"sass": "^1.52.2",
|
||||
"sequelize-cli": "^6.3.0",
|
||||
"supertest": "^6.2.2",
|
||||
"webpack": "4",
|
||||
"webpack-cli": "^4.7.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"source-map-resolve": "0.6.0",
|
||||
"lodash": "4.17.21",
|
||||
"minimist": "1.2.5",
|
||||
"jimp": "0.16.1",
|
||||
"resize-img": "2.0.0",
|
||||
"underscore": "1.13.1",
|
||||
"postcss": "7.0.36",
|
||||
"glob-parent": "5.1.2",
|
||||
"chokidar": "3.5.2",
|
||||
"core-js": "3.19.0"
|
||||
},
|
||||
"bin": {
|
||||
"gancio": "server/cli.js"
|
||||
},
|
||||
|
|
|
@ -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`, '')
|
||||
|
@ -331,10 +332,3 @@ export default {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.main_image {
|
||||
margin: 0 auto;
|
||||
border-radius: 5px;
|
||||
transition: max-height 0.2s;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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>
|
|
@ -6,38 +6,36 @@
|
|||
Announcement(v-for='announcement in announcements' :key='`a_${announcement.id}`' :announcement='announcement')
|
||||
|
||||
//- Calendar and search bar
|
||||
v-row.pt-0.pt-sm-2.pl-0.pl-sm-2
|
||||
#calh.col-xl-5.col-lg-5.col-md-7.col-sm-12.col-xs-12.pa-4.pa-sm-3
|
||||
v-row.ma-2
|
||||
#calh.col-xl-5.col-lg-5.col-md-7.col-sm-12.col-xs-12.pa-0.ma-0
|
||||
//- this is needed as v-calendar does not support SSR
|
||||
//- https://github.com/nathanreyes/v-calendar/issues/336
|
||||
client-only(placeholder='Calendar unavailable without js')
|
||||
Calendar(@dayclick='dayChange' @monthchange='monthChange' :events='filteredEvents')
|
||||
client-only(placeholder='Loading...')
|
||||
Calendar(@dayclick='dayChange' @monthchange='monthChange' :events='events')
|
||||
|
||||
.col.pt-0.pt-md-2
|
||||
Search(:filters='filters' @update='updateFilters')
|
||||
v-chip(v-if='selectedDay' close :close-icon='mdiCloseCircle' @click:close='dayChange()') {{selectedDay}}
|
||||
.col.pt-0.pt-md-2.mt-4.ma-md-0.pb-0
|
||||
//- v-btn(to='/search' color='primary' ) {{$t('common.search')}}
|
||||
v-form(to='/search' action='/search' method='GET')
|
||||
v-text-field(name='search' :label='$t("common.search")' outlined rounded hide-details :append-icon='mdiMagnify')
|
||||
|
||||
//- Events
|
||||
#events.mb-2.mt-1.pl-1.pl-sm-2
|
||||
Event(:event='event' @destroy='destroy' v-for='(event, idx) in visibleEvents' :lazy='idx>2' :key='event.id' @tagclick='tagClick' @placeclick='placeClick')
|
||||
|
||||
Event(:event='event' @destroy='destroy' v-for='(event, idx) in visibleEvents' :lazy='idx>2' :key='event.id')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions } from 'vuex'
|
||||
import intersection from 'lodash/intersection'
|
||||
import { mapState } from 'vuex'
|
||||
import dayjs from 'dayjs'
|
||||
import Event from '@/components/Event'
|
||||
import Announcement from '@/components/Announcement'
|
||||
import Search from '@/components/Search'
|
||||
import Calendar from '@/components/Calendar'
|
||||
import { mdiCloseCircle } from '@mdi/js'
|
||||
import { mdiMagnify } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
name: 'Index',
|
||||
components: { Event, Search, Announcement, Calendar },
|
||||
components: { Event, Announcement, Calendar },
|
||||
middleware: 'setup',
|
||||
async asyncData ({ params, $api, store }) {
|
||||
async asyncData ({ $api }) {
|
||||
const events = await $api.getEvents({
|
||||
start: dayjs().startOf('month').unix(),
|
||||
end: null,
|
||||
|
@ -45,13 +43,13 @@ export default {
|
|||
})
|
||||
return { events }
|
||||
},
|
||||
data ({ $store }) {
|
||||
data () {
|
||||
return {
|
||||
mdiCloseCircle,
|
||||
mdiMagnify,
|
||||
first: true,
|
||||
isCurrentMonth: true,
|
||||
now: dayjs().unix(),
|
||||
date: dayjs().format('YYYY-MM-DD'),
|
||||
date: dayjs.tz().format('YYYY-MM-DD'),
|
||||
events: [],
|
||||
start: dayjs().startOf('month').unix(),
|
||||
end: null,
|
||||
|
@ -74,49 +72,22 @@ export default {
|
|||
]
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(['settings', 'announcements', 'filters']),
|
||||
filteredEvents () {
|
||||
let events = this.events
|
||||
if (!this.filters.places.length && !this.filters.tags.length) {
|
||||
if (this.filters.show_recurrent) {
|
||||
return this.events
|
||||
}
|
||||
events = events.filter(e => !e.parentId)
|
||||
}
|
||||
|
||||
return events.filter(e => {
|
||||
// check tags intersection
|
||||
if (this.filters.tags.length) {
|
||||
const ret = intersection(this.filters.tags, e.tags)
|
||||
if (!ret.length) { return false }
|
||||
}
|
||||
// check if place is in filtered places
|
||||
if (this.filters.places.length && !this.filters.places.includes(e.place.id)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
},
|
||||
...mapState(['settings', 'announcements']),
|
||||
visibleEvents () {
|
||||
const now = dayjs().unix()
|
||||
if (this.selectedDay) {
|
||||
const min = dayjs(this.selectedDay).startOf('day').unix()
|
||||
const max = dayjs(this.selectedDay).endOf('day').unix()
|
||||
return this.filteredEvents.filter(e => (e.start_datetime < max && e.start_datetime > min))
|
||||
return this.events.filter(e => (e.start_datetime <= max && e.start_datetime >= min))
|
||||
} else if (this.isCurrentMonth) {
|
||||
return this.filteredEvents.filter(e => e.end_datetime ? e.end_datetime > now : e.start_datetime + 2 * 60 * 60 > now)
|
||||
return this.events.filter(e => e.end_datetime ? e.end_datetime > now : e.start_datetime + 2 * 60 * 60 > now)
|
||||
} else {
|
||||
return this.filteredEvents
|
||||
return this.events
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// onIntersect (isIntersecting, eventId) {
|
||||
// this.intersecting[eventId] = isIntersecting
|
||||
// },
|
||||
...mapActions(['setFilters']),
|
||||
destroy (id) {
|
||||
this.events = this.events.filter(e => e.id !== id)
|
||||
},
|
||||
|
@ -131,20 +102,6 @@ export default {
|
|||
this.$nuxt.$loading.finish()
|
||||
})
|
||||
},
|
||||
placeClick (place_id) {
|
||||
if (this.filters.places.includes(place_id)) {
|
||||
this.setFilters({ ...this.filters, places: this.filters.places.filter(p_id => p_id !== place_id) })
|
||||
} else {
|
||||
this.setFilters({ ...this.filters, places: [].concat(this.filters.places, place_id) })
|
||||
}
|
||||
},
|
||||
tagClick (tag) {
|
||||
if (this.filters.tags.includes(tag)) {
|
||||
this.setFilters({ ...this.filters, tags: this.filters.tags.filter(t => t !== tag) })
|
||||
} else {
|
||||
this.setFilters({ ...this.filters, tags: [].concat(this.filters.tags, tag) })
|
||||
}
|
||||
},
|
||||
monthChange ({ year, month }) {
|
||||
// avoid first time monthChange event (onload)
|
||||
if (this.first) {
|
||||
|
@ -158,24 +115,20 @@ export default {
|
|||
this.selectedDay = null
|
||||
|
||||
// check if current month is selected
|
||||
if (month - 1 === dayjs().month() && year === dayjs().year()) {
|
||||
if (month - 1 === dayjs.tz().month() && year === dayjs.tz().year()) {
|
||||
this.isCurrentMonth = true
|
||||
this.start = dayjs().startOf('month').unix()
|
||||
this.date = dayjs().format('YYYY-MM-DD')
|
||||
this.date = dayjs.tz().format('YYYY-MM-DD')
|
||||
} else {
|
||||
this.isCurrentMonth = false
|
||||
this.date = ''
|
||||
this.start = dayjs().year(year).month(month - 1).startOf('month').unix() // .startOf('week').unix()
|
||||
}
|
||||
// TODO: check if calendar view is double
|
||||
this.end = dayjs().year(year).month(month).endOf('month').unix() // .endOf('week').unix()
|
||||
this.updateEvents()
|
||||
},
|
||||
updateFilters (filters) {
|
||||
this.setFilters(filters)
|
||||
},
|
||||
dayChange (day) {
|
||||
this.selectedDay = day ? dayjs(day).format('YYYY-MM-DD') : null
|
||||
this.selectedDay = day ? dayjs.tz(day).format('YYYY-MM-DD') : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
30
pages/p/_place.vue
Normal file
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,5 +1,4 @@
|
|||
<template lang="pug">
|
||||
|
||||
v-container.pa-6
|
||||
h2.mb-2.text-center Gancio Setup
|
||||
v-stepper.grey.lighten-5(v-model='step')
|
||||
|
@ -16,17 +15,12 @@
|
|||
v-stepper-content(step='2')
|
||||
Settings(setup, @complete='configCompleted')
|
||||
v-stepper-content(step='3')
|
||||
Completed(ref='completed')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Completed(ref='completed' :isHttp='isHttp')
|
||||
</template>
|
||||
<script>
|
||||
import DbStep from './DbStep'
|
||||
import Settings from '../../components/admin/Settings'
|
||||
import Completed from './Completed'
|
||||
import DbStep from '@/components/DbStep'
|
||||
import Settings from '@/components/admin/Settings'
|
||||
import Completed from '@/components/Completed'
|
||||
|
||||
export default {
|
||||
components: { DbStep, Settings, Completed },
|
||||
|
@ -36,9 +30,10 @@ export default {
|
|||
title: 'Setup',
|
||||
},
|
||||
auth: false,
|
||||
asyncData ({ params }) {
|
||||
|
||||
asyncData ({ params, req }) {
|
||||
const protocol = process.client ? window.location.protocol : req.protocol + ':'
|
||||
return {
|
||||
isHttp: protocol === 'http:',
|
||||
dbdone: !!Number(params.db),
|
||||
config: {
|
||||
db: {
|
||||
|
|
30
pages/tag/_tag.vue
Normal file
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) {
|
||||
try {
|
||||
const old_path = path.resolve(config.upload_path, event.media[0].url)
|
||||
const old_thumb_path = path.resolve(config.upload_path, 'thumb', event.media[0].url)
|
||||
try {
|
||||
fs.unlinkSync(old_path)
|
||||
fs.unlinkSync(old_thumb_path)
|
||||
} catch (e) {
|
||||
log.info(e.toString())
|
||||
}
|
||||
}
|
||||
let url
|
||||
if (req.file) {
|
||||
url = req.file.filename
|
||||
} else if (body.image_url) {
|
||||
if (/^https?:\/\//.test(body.image_url)) {
|
||||
url = await helpers.getImageFromURL(body.image_url)
|
||||
} else {
|
||||
url = body.image_url
|
||||
}
|
||||
|
||||
// modify associated media only if a new file is uploaded or remote image_url is used
|
||||
if (req.file || (body.image_url && /^https?:\/\//.test(body.image_url))) {
|
||||
if (body.image_url) {
|
||||
req.file = await helpers.getImageFromURL(body.image_url)
|
||||
}
|
||||
|
||||
if (url && !event.recurrent) {
|
||||
const focalpoint = body.image_focalpoint ? body.image_focalpoint.split(',') : ['0', '0']
|
||||
eventDetails.media = [{
|
||||
url,
|
||||
name: body.image_name || '',
|
||||
url: req.file.filename,
|
||||
height: req.file.height,
|
||||
width: req.file.width,
|
||||
name: body.image_name || body.title || '',
|
||||
focalpoint: [parseFloat(focalpoint[0].slice(0, 6)), parseFloat(focalpoint[1].slice(0, 6))]
|
||||
}]
|
||||
} else {
|
||||
} else if (!body.image) {
|
||||
eventDetails.media = []
|
||||
}
|
||||
|
||||
await event.update(eventDetails)
|
||||
const [place] = await Place.findOrCreate({
|
||||
where: { name: body.place_name },
|
||||
|
@ -481,9 +560,9 @@ const eventController = {
|
|||
// check if event is mine (or user is admin)
|
||||
if (event && (res.locals.user.is_admin || res.locals.user.id === event.userId)) {
|
||||
if (event.media && event.media.length && !event.recurrent) {
|
||||
try {
|
||||
const old_path = path.join(config.upload_path, event.media[0].url)
|
||||
const old_thumb_path = path.join(config.upload_path, 'thumb', event.media[0].url)
|
||||
try {
|
||||
fs.unlinkSync(old_thumb_path)
|
||||
fs.unlinkSync(old_path)
|
||||
} catch (e) {
|
||||
|
@ -523,22 +602,22 @@ const eventController = {
|
|||
if (!show_recurrent) {
|
||||
where.parentId = null
|
||||
}
|
||||
|
||||
if (end) {
|
||||
where.start_datetime = { [Op.lte]: end }
|
||||
}
|
||||
|
||||
const replacements = []
|
||||
if (tags && places) {
|
||||
where[Op.or] = {
|
||||
placeId: places ? places.split(',') : [],
|
||||
'$tags.tag$': tags.split(',')
|
||||
// '$tags.tag$': Sequelize.literal(`EXISTS (SELECT 1 FROM event_tags WHERE tagTag in ( ${Sequelize.QueryInterface.escape(tags)} ) )`)
|
||||
}
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
where['$tags.tag$'] = tags.split(',')
|
||||
}
|
||||
|
||||
if (places) {
|
||||
} else if (tags) {
|
||||
// where[Op.and] = Sequelize.literal(`EXISTS (SELECT 1 FROM event_tags WHERE eventId=event.id AND tagTag in (?))`)
|
||||
where[Op.and] = Sequelize.fn('EXISTS', Sequelize.literal('SELECT 1 FROM event_tags WHERE "event_tags"."eventId"="event".id AND "tagTag" in (?)'))
|
||||
replacements.push(tags)
|
||||
} else if (places) {
|
||||
where.placeId = places.split(',')
|
||||
}
|
||||
|
||||
|
@ -554,12 +633,12 @@ const eventController = {
|
|||
model: Tag,
|
||||
order: [Sequelize.literal('(SELECT COUNT("tagTag") FROM event_tags WHERE tagTag = tag) DESC')],
|
||||
attributes: ['tag'],
|
||||
required: !!tags,
|
||||
through: { attributes: [] }
|
||||
},
|
||||
{ model: Place, required: true, attributes: ['id', 'name', 'address'] }
|
||||
],
|
||||
limit: max
|
||||
limit: max,
|
||||
replacements
|
||||
}).catch(e => {
|
||||
log.error('[EVENT]', e)
|
||||
return []
|
||||
|
|
|
@ -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', () => {
|
||||
file.stream.pipe(sharpStream)
|
||||
Promise.all(promises)
|
||||
.then(res => {
|
||||
const info = res[1]
|
||||
cb(null, {
|
||||
destination: config.upload_path,
|
||||
filename,
|
||||
path: finalPath,
|
||||
size: outStream.bytesWritten
|
||||
filename: filename + '.jpg',
|
||||
path: path.resolve(config.upload_path, filename + '.jpg'),
|
||||
height: info.height,
|
||||
width: info.width,
|
||||
size: info.size,
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err)
|
||||
req.err = err
|
||||
cb(null)
|
||||
})
|
||||
},
|
||||
_removeFile (req, file, cb) {
|
||||
_removeFile (_req, file, cb) {
|
||||
delete file.destination
|
||||
delete file.filename
|
||||
fs.unlink(file.path, cb)
|
||||
|
|
|
@ -22,16 +22,17 @@ require('yargs')
|
|||
.option('config', {
|
||||
alias: 'c',
|
||||
describe: 'Configuration file',
|
||||
default: path.resolve(process.env.cwd, 'config.json')
|
||||
})
|
||||
.coerce('config', config_path => {
|
||||
default: path.resolve(process.env.cwd, 'config.json'),
|
||||
coerce: config_path => {
|
||||
const absolute_config_path = path.resolve(process.env.cwd, config_path)
|
||||
process.env.config_path = absolute_config_path
|
||||
return absolute_config_path
|
||||
})
|
||||
.command(['accounts'], 'Manage accounts', accountsCLI)
|
||||
}})
|
||||
.command(['start', 'run', '$0'], 'Start gancio', {}, start)
|
||||
.command(['accounts'], 'Manage accounts', accountsCLI)
|
||||
.help('h')
|
||||
.alias('h', 'help')
|
||||
.epilog('Made with ❤ by underscore hacklab - https://gancio.org')
|
||||
.recommendCommands()
|
||||
.demandCommand(1, '')
|
||||
.argv
|
||||
|
|
|
@ -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
|
|
@ -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,40 +121,33 @@ module.exports = {
|
|||
if(!/^https?:\/\//.test(url)) {
|
||||
throw Error('Hacking attempt?')
|
||||
}
|
||||
const filename = crypto.randomBytes(16).toString('hex') + '.jpg'
|
||||
const finalPath = path.resolve(config.upload_path, filename)
|
||||
const thumbPath = path.resolve(config.upload_path, 'thumb', filename)
|
||||
const outStream = fs.createWriteStream(finalPath)
|
||||
const thumbStream = fs.createWriteStream(thumbPath)
|
||||
|
||||
const resizer = sharp().resize(1200).jpeg({ quality: 95 })
|
||||
const thumbnailer = sharp().resize(400).jpeg({ quality: 90 })
|
||||
const filename = crypto.randomBytes(16).toString('hex')
|
||||
const sharpStream = sharp({ failOnError: true })
|
||||
const promises = [
|
||||
sharpStream.clone().resize(500, null, { withoutEnlargement: true }).jpeg({ effort: 6, mozjpeg: true }).toFile(path.resolve(config.upload_path, 'thumb', filename + '.jpg')),
|
||||
sharpStream.clone().resize(1200, null, { withoutEnlargement: true } ).jpeg({ quality: 95, effort: 6, mozjpeg: true}).toFile(path.resolve(config.upload_path, filename + '.jpg')),
|
||||
]
|
||||
|
||||
const response = await axios({ method: 'GET', url, responseType: 'stream' })
|
||||
const response = await axios({ method: 'GET', url: encodeURI(url), responseType: 'stream' })
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let onError = false
|
||||
const err = e => {
|
||||
if (onError) {
|
||||
return
|
||||
response.data.pipe(sharpStream)
|
||||
return Promise.all(promises)
|
||||
.then(res => {
|
||||
const info = res[1]
|
||||
return {
|
||||
destination: config.upload_path,
|
||||
filename: filename + '.jpg',
|
||||
path: path.resolve(config.upload_path, filename + '.jpg'),
|
||||
height: info.height,
|
||||
width: info.width,
|
||||
size: info.size,
|
||||
}
|
||||
onError = true
|
||||
reject(e)
|
||||
}
|
||||
|
||||
response.data
|
||||
.pipe(thumbnailer)
|
||||
.on('error', err)
|
||||
.pipe(thumbStream)
|
||||
.on('error', err)
|
||||
|
||||
response.data
|
||||
.pipe(resizer)
|
||||
.on('error', err)
|
||||
.pipe(outStream)
|
||||
.on('error', err)
|
||||
|
||||
outStream.on('finish', () => resolve(filename))
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err)
|
||||
req.err = err
|
||||
cb(null)
|
||||
})
|
||||
},
|
||||
|
||||
|
@ -232,7 +224,8 @@ module.exports = {
|
|||
},
|
||||
|
||||
async APRedirect (req, res, next) {
|
||||
if (!req.accepts('html')) {
|
||||
const acceptJson = req.accepts('html', 'application/activity+json') === 'application/activity+json'
|
||||
if (acceptJson) {
|
||||
const eventController = require('../server/api/controller/event')
|
||||
const event = await eventController._get(req.params.slug)
|
||||
if (event) {
|
||||
|
|
|
@ -1,14 +1,29 @@
|
|||
|
||||
module.exports = function () {
|
||||
const config = require('../server/config')
|
||||
config.load()
|
||||
|
||||
const initialize = {
|
||||
// close connections/port/unix socket
|
||||
async shutdown (exit = true) {
|
||||
const log = require('../server/log')
|
||||
const TaskManager = require('../server/taskManager').TaskManager
|
||||
if (TaskManager) { TaskManager.stop() }
|
||||
log.info('Closing DB')
|
||||
const sequelize = require('../server/api/models')
|
||||
await sequelize.close()
|
||||
process.off('SIGTERM', initialize.shutdown)
|
||||
process.off('SIGINT', initialize.shutdown)
|
||||
if (exit) {
|
||||
process.exit()
|
||||
}
|
||||
},
|
||||
|
||||
async start () {
|
||||
const log = require('../server/log')
|
||||
const settingsController = require('./api/controller/settings')
|
||||
const db = require('./api/models/index')
|
||||
const dayjs = require('dayjs')
|
||||
const timezone = require('dayjs/plugin/timezone')
|
||||
async function start (nuxt) {
|
||||
|
||||
dayjs.extend(timezone)
|
||||
if (config.status == 'READY') {
|
||||
await db.initialize()
|
||||
} else {
|
||||
|
@ -18,18 +33,21 @@ module.exports = function () {
|
|||
dialect: process.env.GANCIO_DB_DIALECT,
|
||||
storage: process.env.GANCIO_DB_STORAGE,
|
||||
host: process.env.GANCIO_DB_HOST,
|
||||
port: process.env.GANCIO_DB_PORT,
|
||||
database: process.env.GANCIO_DB_DATABASE,
|
||||
username: process.env.GANCIO_DB_USERNAME,
|
||||
password: process.env.GANCIO_DB_PASSWORD,
|
||||
}
|
||||
|
||||
setupController._setupDb(dbConf)
|
||||
.catch(e => { process.exit(1) })
|
||||
.catch(e => {
|
||||
log.warn(String(e))
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
await settingsController.load()
|
||||
}
|
||||
|
||||
dayjs.extend(timezone)
|
||||
dayjs.tz.setDefault(settingsController.settings.instance_timezone)
|
||||
|
||||
let TaskManager
|
||||
|
@ -37,22 +55,11 @@ module.exports = function () {
|
|||
TaskManager = require('../server/taskManager').TaskManager
|
||||
TaskManager.start()
|
||||
}
|
||||
log.info(`Listen on ${config.server.host}:${config.server.port}`)
|
||||
|
||||
// close connections/port/unix socket
|
||||
async function shutdown () {
|
||||
if (TaskManager) { TaskManager.stop() }
|
||||
log.info('Closing DB')
|
||||
const sequelize = require('../server/api/models')
|
||||
await sequelize.close()
|
||||
process.off('SIGTERM', shutdown)
|
||||
process.off('SIGINT', shutdown)
|
||||
nuxt.close()
|
||||
process.exit()
|
||||
process.on('SIGTERM', initialize.shutdown)
|
||||
process.on('SIGINT', initialize.shutdown)
|
||||
}
|
||||
process.on('SIGTERM', shutdown)
|
||||
process.on('SIGINT', shutdown)
|
||||
}
|
||||
|
||||
return start(this.nuxt)
|
||||
}
|
||||
|
||||
module.exports = initialize
|
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,11 +37,12 @@ if (config.status === 'READY') {
|
|||
|
||||
// rss/ics/atom feed
|
||||
app.get('/feed/:type', cors(), exportController.export)
|
||||
app.use('/.well-known', webfinger)
|
||||
|
||||
app.use('/event/:slug', helpers.APRedirect)
|
||||
|
||||
// federation api / activitypub / webfinger / nodeinfo
|
||||
app.use('/federation', federation)
|
||||
app.use('/.well-known', webfinger)
|
||||
|
||||
// ignore unimplemented ping url from fediverse
|
||||
app.use(spamFilter)
|
||||
|
@ -54,26 +58,34 @@ if (config.status === 'READY') {
|
|||
app.use('/api', api)
|
||||
|
||||
// // Handle 500
|
||||
app.use((error, req, res, next) => {
|
||||
app.use((error, _req, res, _next) => {
|
||||
log.error('[ERROR]', error)
|
||||
res.status(500).send('500: Internal Server Error')
|
||||
return res.status(500).send('500: Internal Server Error')
|
||||
})
|
||||
|
||||
// remaining request goes to nuxt
|
||||
// first nuxt component is ./pages/index.vue (with ./layouts/default.vue)
|
||||
// prefill current events, tags, places and announcements (used in every path)
|
||||
app.use(async (req, res, next) => {
|
||||
// const start_datetime = getUnixTime(startOfWeek(startOfMonth(new Date())))
|
||||
// req.events = await eventController._select(start_datetime, 100)
|
||||
if (config.status === 'READY') {
|
||||
|
||||
const eventController = require('./api/controller/event')
|
||||
const announceController = require('./api/controller/announce')
|
||||
res.locals.meta = await eventController._getMeta()
|
||||
res.locals.announcements = await announceController._getVisible()
|
||||
}
|
||||
res.locals.status = config.status
|
||||
next()
|
||||
})
|
||||
|
||||
module.exports = app
|
||||
module.exports = {
|
||||
handler: app,
|
||||
load () {
|
||||
console.error('dentro load !')
|
||||
},
|
||||
unload: () => initialize.shutdown(false)
|
||||
// async unload () {
|
||||
// const db = require('./api/models/index')
|
||||
// await db.close()
|
||||
// process.off('SIGTERM')
|
||||
// process.off('SIGINT')
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -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