stream of consciousness

This commit is contained in:
lesion 2019-03-07 14:59:28 +01:00
parent 8c3642e868
commit 5883589e19
35 changed files with 347 additions and 409 deletions

View file

@ -1 +1,2 @@
**/node_modules
client/node_modules
node_modules

3
.env
View file

@ -1,5 +1,8 @@
BASE_URL=http://localhost:12300
TITLE=Gancio
DESCRIPTION=diocane
ADMIN_EMAIL=admin@example.com
SMTP_HOST=mail.example.com

View file

@ -6,7 +6,7 @@ WORKDIR /usr/src/app
COPY package.json .
# install backend dependencies
RUN yarn install
RUN yarn
# copy source
COPY . .

View file

@ -1,6 +1,5 @@
const express = require('express')
const { isAuth, isAdmin } = require('./auth')
const { fillUser, isAuth, isAdmin } = require('./auth')
const eventController = require('./controller/event')
const exportController = require('./controller/export')
// const botController = require('./controller/bot')
@ -12,8 +11,7 @@ const api = express.Router()
// USER API
const userController = require('./controller/user')
api.route('/login')
.post(userController.login)
api.post('/login', userController.login)
api.route('/user')
.post(userController.register)
@ -25,23 +23,22 @@ api.put('/tag', isAuth, isAdmin, eventController.updateTag)
api.put('/place', isAuth, isAdmin, eventController.updatePlace)
api.route('/user/event')
.post(isAuth, upload.single('image'), userController.addEvent)
.post(fillUser, upload.single('image'), userController.addEvent)
.get(isAuth, userController.getMyEvents)
.put(isAuth, upload.single('image'), userController.updateEvent)
api.route('/user/event/:id')
.delete(isAuth, userController.delEvent)
api.delete('/user/event/:id', isAuth, userController.delEvent)
api.get('/event/meta', eventController.getMeta)
api.route('/event/:event_id')
.get(eventController.get)
api.get('/event/unconfirmed', isAuth, isAdmin, eventController.getUnconfirmed)
api.post('/event/reminder', eventController.addReminder)
api.get('/event/:event_id', eventController.get)
api.get('/event/confirm/:event_id', isAuth, isAdmin, eventController.confirm)
// api.get('/export/feed', exportController.feed)
// api.get('/export/ics', exportController.ics)
api.get('/export/:type', exportController.export)
api.route('/event/:year/:month')
.get(eventController.getAll)
api.get('/event/:year/:month', eventController.getAll)
api.post('/user/getauthurl', isAuth, userController.getAuthURL)
api.post('/user/code', isAuth, userController.code)

View file

@ -3,6 +3,16 @@ const config = require('./config')
const User = require('./models/user')
const Auth = {
async fillUser (req, res, next) {
const token = req.body.token || req.params.token || req.headers['x-access-token']
console.log('[AUTH] ', token)
if (!token) next()
jwt.verify(token, config.secret, async (err, decoded) => {
if (err) next()
req.user = await User.findOne({ where: { email: decoded.email, is_active: true } })
next()
})
},
async isAuth (req, res, next) {
const token = req.body.token || req.params.token || req.headers['x-access-token']
console.log('[AUTH] ', token)

View file

@ -1,4 +1,7 @@
/* backend configuration */
let db = {}
let apiurl
if (process.env.NODE_ENV === 'production') {
db = {
host: process.env.DB_HOST,
@ -7,11 +10,13 @@ if (process.env.NODE_ENV === 'production') {
database: process.env.DB_NAME,
dialect: 'postgres'
}
apiurl = process.env.BASE_URL + '/api'
} else {
db = {
dialect: 'sqlite',
storage: './db.sqlite'
}
apiurl = 'http://localhost:9000'
}
module.exports = {
@ -21,7 +26,7 @@ module.exports = {
description: process.env.DESCRIPTION || 'A calendar for radical communities',
baseurl: process.env.BASE_URL || 'http://localhost:8080',
apiurl: process.env.API_URL || 'http://localhost:9000',
apiurl,
db,
admin: process.env.ADMIN_EMAIL,

View file

@ -7,64 +7,65 @@ const moment = require('moment')
moment.locale('it')
const botController = {
bots: [],
async initialize () {
console.log('initialize bots')
const botUsers = await User.findAll({ where: { mastodon_auth: { [Op.ne]: null } } })
console.log(botUsers)
botController.bots = botUsers.map(user => {
console.log('initialize bot ', user.name)
console.log('.. ', user.mastodon_auth)
const { client_id, client_secret, access_token } = user.mastodon_auth
const bot = new Mastodon({ access_token, api_url: `https://${user.mastodon_instance}/api/v1/` })
const listener = bot.stream('streaming/direct')
listener.on('message', botController.message)
listener.on('error', botController.error)
return { email: user.email, bot }
})
console.log(botController.bots)
},
add (user, token) {
const bot = new Mastodon({ access_token: user.mastodon_auth.access_token, api_url: `https://${user.mastodon_instance}/api/v1/` })
const listener = bot.stream('streaming/direct')
listener.on('message', botController.message)
listener.on('error', botController.error)
botController.bots.push({ email: user.email, bot })
},
// async initialize () {
// console.log('initialize bots')
// const botUsers = await User.findAll({ where: { mastodon_auth: { [Op.ne]: null } } })
// console.log(botUsers)
// botController.bots = botUsers.map(user => {
// console.log('initialize bot ', user.name)
// console.log('.. ', user.mastodon_auth)
// const { client_id, client_secret, access_token } = user.mastodon_auth
// const bot = new Mastodon({ access_token, api_url: `https://${user.mastodon_instance}/api/v1/` })
// const listener = bot.stream('streaming/direct')
// listener.on('message', botController.message)
// listener.on('error', botController.error)
// return { email: user.email, bot }
// })
// console.log(botController.bots)
// },
// add (user, token) {
// const bot = new Mastodon({ access_token: user.mastodon_auth.access_token, api_url: `https://${user.mastodon_instance}/api/v1/` })
// const listener = bot.stream('streaming/direct')
// listener.on('message', botController.message)
// listener.on('error', botController.error)
// botController.bots.push({ email: user.email, bot })
// },
post (user, event) {
const { bot } = botController.bots.filter(b => b.email === user.email)[0]
const { client_id, client_secret, access_token } = user.mastodon_auth
const bot = new Mastodon({ access_token, api_url: `https://${user.mastodon_instance}/api/v1/` })
const status = `${event.title} @ ${event.place.name} ${moment(event.start_datetime).format('ddd, D MMMM HH:mm')} -
${event.description} - ${event.tags.map(t => '#' + t.tag).join(' ')} ${config.baseurl}/event/${event.id}`
return bot.post('/statuses', { status, visibility: 'private' })
},
async message (msg) {
console.log(msg)
console.log(msg.data.accounts)
const replyid = msg.data.in_reply_to_id || msg.data.last_status.in_reply_to_id
if (!replyid) return
// const event = await Event.findOne({ where: { activitypub_id: replyid } })
// if (!event) {
// check for comment..
// const comment = await Comment.findOne( {where: { }})
// }
// const comment = await Comment.create({activitypub_id: msg.data.last_status.id, text: msg.data.last_status.content, author: msg.data.accounts[0].username })
// event.addComment(comment)
// console.log(event)
// const comment = await Comment.findOne( { where: {activitypub_id: msg.data.in_reply_to}} )
// console.log('dentro message ', data)
// add comment to specified event
// let comment
// if (!event) {
// const comment = await Comment.findOne({where: {activitypub_id: req.body.id}, include: Event})
// event = comment.event
// }
// const comment = new Comment(req.body)
// event.addComment(comment)
},
error (err) {
console.log('error ', err)
}
// async message (msg) {
// console.log(msg)
// console.log(msg.data.accounts)
// const replyid = msg.data.in_reply_to_id || msg.data.last_status.in_reply_to_id
// if (!replyid) return
// const event = await Event.findOne({ where: { activitypub_id: replyid } })
// if (!event) {
// check for comment..
// const comment = await Comment.findOne( {where: { }})
// }
// const comment = await Comment.create({activitypub_id: msg.data.last_status.id, text: msg.data.last_status.content, author: msg.data.accounts[0].username })
// event.addComment(comment)
// console.log(event)
// const comment = await Comment.findOne( { where: {activitypub_id: msg.data.in_reply_to}} )
// console.log('dentro message ', data)
// add comment to specified event
// let comment
// if (!event) {
// const comment = await Comment.findOne({where: {activitypub_id: req.body.id}, include: Event})
// event = comment.event
// }
// const comment = new Comment(req.body)
// event.addComment(comment)
// },
// error (err) {
// console.log('error ', err)
// }
}
setTimeout(botController.initialize, 2000)
// setTimeout(botController.initialize, 2000)
module.exports = botController

View file

@ -1,4 +1,4 @@
const { User, Event, Comment, Tag, Place } = require('../model')
const { User, Event, Comment, Tag, Place, MailReminder } = require('../model')
const moment = require('moment')
const Sequelize = require('sequelize')
@ -32,22 +32,51 @@ const eventController = {
res.send(404)
}
},
async updatePlace (req, res) {
const place = await Place.findByPk(req.body.id)
await place.update(req.body)
res.json(place)
},
async get (req, res) {
const id = req.params.event_id
const event = await Event.findByPk(id, { include: [User, Tag, Comment, Place] })
res.json(event)
},
async confirm (req, res) {
const id = req.params.event_id
const event = await Event.findByPk(id)
try {
await event.update({ is_visible: true })
res.send(200)
} catch (e) {
res.send(404)
}
},
async getUnconfirmed (req, res) {
const events = await Event.findAll({
where: {
is_visible: false
},
order: [['start_datetime', 'ASC']],
include: [Tag, Place]
})
res.json(events)
},
async addReminder (req, res) {
await MailReminder.create(req.body.reminder)
res.json(200)
},
async getAll (req, res) {
const start = moment().year(req.params.year).month(req.params.month).startOf('month').subtract(1, 'week')
const end = moment().year(req.params.year).month(req.params.month).endOf('month').add(1, 'week')
const events = await Event.findAll({
where: {
is_visible: true,
[Sequelize.Op.and]: [
{ start_datetime: { [Sequelize.Op.gte]: start } },
{ start_datetime: { [Sequelize.Op.lte]: end } }

View file

@ -15,10 +15,9 @@ const userController = {
res.status(404).json({ success: false, message: 'AUTH_FAIL' })
} else if (user) {
if (!user.is_active) {
res.status(403).json({ success: false, message: 'NOT)CONFIRMED' })
}
res.status(403).json({ success: false, message: 'NOT_CONFIRMED' })
// check if password matches
else if (!await user.comparePassword(req.body.password)) {
} else if (!await user.comparePassword(req.body.password)) {
res.status(403).json({ success: false, message: 'AUTH_FAIL' })
} else {
// if user is found and password is right
@ -48,21 +47,26 @@ const userController = {
await event.destroy()
res.sendStatus(200)
} else {
res.sendStatus(404)
res.sendStatus(403)
}
},
// ADD EVENT
async addEvent (req, res) {
const body = req.body
// remove description tag and create anchor tag
const description = body.description
.replace(/(<([^>]+)>)/ig, '')
.replace(/(https?:\/\/[^\s]+)/g, '<a href="$1">$1</a>')
const eventDetails = {
title: body.title,
description,
multidate: body.multidate,
start_datetime: body.start_datetime,
end_datetime: body.end_datetime
end_datetime: body.end_datetime,
is_visible: req.user ? true : false
}
if (req.file) {
@ -88,14 +92,18 @@ const userController = {
const tags = await Tag.findAll({ where: { tag: body.tags } })
await event.addTags(tags)
}
await req.user.addEvent(event)
if (req.user) await req.user.addEvent(event)
event = await Event.findByPk(event.id, { include: [User, Tag, Place] })
// check if bot exists
if (req.user.mastodon_auth) {
if (req.user && req.user.mastodon_auth) {
const post = await bot.post(req.user, event)
event.activitypub_id = post.id
event.save()
}
mail.send(config.admin, 'event', { event })
return res.json(event)
},

View file

@ -1,4 +1,4 @@
const User = require('./models/user')
const { Event, Comment, Tag, Place } = require('./models/event')
const { Event, Comment, Tag, Place, MailReminder } = require('./models/event')
module.exports = { User, Event, Comment, Tag, Place }
module.exports = { User, Event, Comment, Tag, Place, MailReminder }

View file

@ -4,12 +4,13 @@ const User = require('./user')
const Event = db.define('event', {
title: Sequelize.STRING,
description: Sequelize.STRING,
description: Sequelize.TEXT,
multidate: Sequelize.BOOLEAN,
start_datetime: { type: Sequelize.DATE, index: true },
end_datetime: { type: Sequelize.DATE, index: true },
image_path: Sequelize.STRING,
activitypub_id: { type: Sequelize.INTEGER, index: true }
activitypub_id: { type: Sequelize.INTEGER, index: true },
is_visible: Sequelize.BOOLEAN
})
const Tag = db.define('tag', {
@ -23,11 +24,11 @@ const Comment = db.define('comment', {
text: Sequelize.STRING
})
const MailSubscription = db.define('subscription', {
const MailReminder = db.define('reminder', {
filters: Sequelize.JSON,
mail: Sequelize.TEXT,
mail: Sequelize.STRING,
send_on_add: Sequelize.BOOLEAN,
send_reminder: Sequelize.INTEGER
send_reminder: Sequelize.BOOLEAN
})
const Place = db.define('place', {
@ -47,4 +48,4 @@ Event.belongsTo(Place)
User.hasMany(Event)
Place.hasMany(Event)
module.exports = { Event, Comment, Tag, Place, MailSubscription }
module.exports = { Event, Comment, Tag, Place, MailReminder }

View file

@ -1,3 +0,0 @@
VUE_BASE_URL=https://localhost:8080/
VUE_INSTANCE_API=https://localhost:9000/
VUE_APP_TITLE=Eventi

View file

@ -1,4 +1,17 @@
# Gancio
an event manager for local communities
##Install
We provide a docker way to run **gancio**.
```
git clone
```
##Development
```
```
## Project setup
```
@ -13,9 +26,4 @@ yarn run serve
### Compiles and minifies for production
```
yarn run build
```
### Lints and fixes files
```
yarn run lint
```
```

View file

@ -5,6 +5,7 @@
transition(name="fade" mode="out-in")
router-view(name='modal')
</template>
<script>
import moment from 'moment'
import api from '@/api'
@ -14,7 +15,6 @@ import Login from '@/components/Login'
import Settings from '@/components/Settings'
import newEvent from '@/components/newEvent'
import eventDetail from '@/components/EventDetail'
import Timeline from '@/components/Timeline'
import Home from '@/components/Home'
import Nav from '@/components/Nav'
@ -30,7 +30,7 @@ export default {
<style>
#logo {
max-height: 60px;
max-height: 40px;
}
.navbar-brand {

View file

@ -44,6 +44,9 @@ export default {
login: (email, password) => post('/login', { email, password }),
register: user => post('/user', user),
getAllEvents: (month, year) => get(`/event/${year}/${month}/`),
getUnconfirmedEvents: () => get('/event/unconfirmed'),
confirmEvent: id => get(`/event/confirm/${id}`),
emailReminder: reminder => post('/event/reminder', reminder),
addEvent: event => post('/user/event', event),
updateEvent: event => put('/user/event', event),
updatePlace: place => put('/place', place),

View file

@ -1,6 +1,7 @@
<template lang="pug">
b-modal(@hidden='$router.replace("/")' :title='$t("Admin")' :visible='true' size='lg')
b-modal(hide-footer @hidden='$router.replace("/")' :title='$t("Admin")' :visible='true' size='lg')
el-tabs
//- USERS
el-tab-pane.pt-1
template(slot='label')
v-icon(name='users')
@ -10,12 +11,14 @@
template(slot='action' slot-scope='data')
el-button.mr-1(size='mini' :type='data.item.is_active?"warning":"success"' @click='toggle(data.item)') {{data.item.is_active?$t('Deactivate'):$t('Activate')}}
el-button(size='mini' :type='data.item.is_admin?"danger":"warning"' @click='toggleAdmin(data.item)') {{data.item.is_admin?$t('Remove Admin'):$t('Admin')}}
b-pagination(:per-page='5' v-model='userPage' :total-rows='users.length')
el-pagination(:page-size='perPage' :currentPage.sync='userPage' :total='users.length')
//- PLACES
el-tab-pane.pt-1
template(slot='label')
v-icon(name='map-marker-alt')
span.ml-1 {{$t('Places')}}
p You can change place's name or address
p {{$t('admin_place_explanation')}}
el-form.mb-2(:inline='true' label-width='120px')
el-form-item(:label="$t('Name')")
el-input.mr-1(:placeholder='$t("Name")' v-model='place.name')
@ -25,22 +28,45 @@
b-table(selectable :items='places' :fields='placeFields' striped hover
small selectedVariant='success' primary-key='id'
select-mode="single" @row-selected='placeSelected'
:per-page='5' :current-page='placePage')
b-pagination(:per-page='5' v-model='placePage' :total-rows='places.length')
:per-page='perPage' :current-page='placePage')
el-pagination(:page-size='perPage' :currentPage.sync='placePage' :total='places.length')
//- EVENTS
el-tab-pane.pt-1
template(slot='label')
v-icon(name='calendar')
span.ml-1 {{$t('Events')}}
p {{$t('event_confirm_explanation')}}
el-table(:data='paginatedEvents' small primary-key='id')
el-table-column(:label='$t("Name")')
template(slot-scope='data') {{data.row.title}}
el-table-column(:label='$t("Where")')
template(slot-scope='data') {{data.row.place.name}}
el-table-column(:label='$t("Confirm")')
template(slot-scope='data')
el-button(type='primary' @click='confirm(data.row.id)' size='mini') {{$t('Confirm')}}
el-button(type='success' @click='preview(data.row.id)' size='mini') {{$t('Preview')}}
el-pagination(:page-size='perPage' :currentPage.sync='eventPage' :total='events.length')
//- TAGS
el-tab-pane.pt-1
template(slot='label')
v-icon(name='tag')
span {{$t('Tags')}}
p You can choose colors of your tags
el-table(:data='tags' striped small hover :per-page='10' :current-page='tagPage')
p Select a tag to change it's color
el-tag(v-if='tag.tag' :color='tag.color || "grey"' size='mini') {{tag.tag}}
el-form.mb-2(:inline='true' label-width='120px')
el-form-item(:label="$t('Color')")
el-color-picker(v-model='tag.color' @change='updateColor')
el-table(:data='paginatedTags' striped small hover
highlight-current-row @current-change="tagSelected")
el-table-column(label='Tag')
template(slot-scope='data')
el-tag(:color='data.row.color' size='mini') {{data.row.tag}}
el-table-column(label='Color')
template(slot-scope='data')
el-color-picker(v-model='data.row.color' @change='updateColor(data.row)')
b-pagination(:per-page='10' v-model='tagPage' :total-rows='tags.length')
el-tag(:color='data.row.color || "grey"' size='mini') {{data.row.tag}}
el-pagination(:page-size='perPage' :currentPage.sync='tagPage' :total='tags.length')
//- SETTINGS
el-tab-pane.pt-1
template(slot='label')
v-icon(name='tools')
@ -55,31 +81,54 @@ export default {
name: 'Admin',
data () {
return {
perPage: 10,
users: [],
userFields: ['email', 'action'],
placeFields: ['name', 'address'],
placePage: 1,
userPage: 1,
eventPage: 1,
tagPage: 1,
tagFields: ['tag', 'color'],
description: '',
place: {name: '', address: '' }
place: {name: '', address: '' },
tag: {name: '', color: ''},
events: [],
}
},
async mounted () {
this.users = await api.getUsers()
this.events = await api.getUnconfirmedEvents()
},
computed: {
...mapState(['tags', 'places']),
paginatedEvents () {
console.log(this.events)
return this.events.slice((this.eventPage-1) * this.perPage,
this.eventPage * this.perPage)
},
paginatedTags () {
return this.tags.slice((this.tagPage-1) * this.perPage,
this.tagPage * this.perPage)
}
},
computed: mapState(['tags', 'places']),
methods: {
placeSelected (items) {
console.log('dentro place selected ', items, items.length)
if (items.length === 0 ) {
this.place.name = this.place.address = ''
return
}
const item = items[0]
this.place.name = item.name
this.place.address = item.address
this.place.id = item.id
},
tagSelected (tag) {
this.tag = tag
},
async savePlace () {
const place = await api.updatePlace(this.place)
},
async toggle(user) {
user.is_active = !user.is_active
@ -89,8 +138,23 @@ export default {
user.is_admin = !user.is_admin
const newuser = await api.updateUser(user)
},
async updateColor(tag) {
const newTag = await api.updateTag(tag)
async updateColor () {
const newTag = await api.updateTag(this.tag)
},
preview (id) {
this.$router.push(`/event/${id}`)
},
async confirm (id) {
console.log('dentro confirm', id)
try {
await api.confirmEvent(id)
this.$message({
message: this.$t('event_confirmed'),
type: 'success'
})
this.events = this.events.filter(e => e.id !== id)
} catch (e) {
}
}
}
}

View file

@ -84,4 +84,8 @@ export default {
margin-bottom: 0em;
margin-top: 0.3em;
}
#calendar a {
color: blue;
}
</style>

View file

@ -6,20 +6,22 @@
div <v-icon name='clock'/> {{event.start_datetime|datetime}}
span <v-icon name='map-marker-alt'/> {{event.place.name}}
br
el-tag.mr-1(:color='tag.color' v-for='tag in event.tags'
size='mini' @click.stop='addSearchTag(tag)') {{tag.tag}}
el-tag.mr-1(:color='tag.color' v-for='tag in event.tags' :key='tag.tag'
size='small' @click.stop='addSearchTag(tag)') {{tag.tag}}
</template>
<script>
import { mapState, mapActions } from 'vuex';
import api from '@/api'
import filters from '@/filters'
console.log(process.env)
export default {
props: ['event'],
computed: {
...mapState(['user']),
imgPath () {
return this.event.image_path && this.event.image_path
return this.event.image_path && process.env.VUE_APP_API + '/' + this.event.image_path
},
mine () {
return this.event.userId === this.user.id

View file

@ -1,8 +1,9 @@
<template lang="pug">
b-modal#eventDetail(hide-body hide-header hide-footer @hidden='$router.replace("/")' size='lg' :visible='true')
b-card(bg-variant='dark' href='#' text-variant='white'
no-body, :img-src='imgPath')
b-card-header
b-modal#eventDetail(ref='eventDetail' hide-body hide-header hide-footer @hidden='$router.replace("/")' size='lg' :visible='true')
b-card(no-body, :img-src='imgPath' v-loading='loading')
el-button.close_button(circle icon='el-icon-close' type='success'
@click='$refs.eventDetail.hide()')
b-card-header
h3 {{event.title}}
v-icon(name='clock')
span {{event.start_datetime|datetime}}
@ -14,11 +15,13 @@
pre(v-html='event.description')
br
el-tag.mr-1(:color='tag.color' v-for='tag in event.tags'
size='mini') {{tag.tag}}
size='mini' :key='tag.tag') {{tag.tag}}
.ml-auto(v-if='mine')
hr
el-button(plain type='danger' @click.prevent='remove' icon='el-icon-remove') {{$t('Remove')}}
el-button(plain type='primary' @click='$router.replace("/edit/"+event.id)') <v-icon color='orange' name='edit'/> {{$t('Edit')}}
//- COMMENTS ...
//- b-navbar(type="dark" variant="dark" toggleable='lg')
//- template(slot='footer')
//- b-navbar-nav
@ -31,6 +34,7 @@
//- b-card-footer(v-for='comment in event.comments')
strong {{comment.author}}
div(v-html='comment.text')
</template>
<script>
import { mapState, mapActions } from 'vuex';
@ -38,10 +42,11 @@ import api from '@/api'
import filters from '@/filters'
export default {
name: 'EventDetail',
computed: {
...mapState(['user']),
imgPath () {
return this.event.image_path && this.event.image_path
return this.event.image_path && process.env.VUE_APP_API + '/' + this.event.image_path
},
mine () {
return this.event.userId === this.user.id || this.user.is_admin
@ -49,7 +54,7 @@ export default {
},
data () {
return {
event: { comments: [], place: {}},
event: { comments: [], place: {}, title: ''},
id: null,
loading: true,
}
@ -58,7 +63,7 @@ export default {
this.id = this.$route.params.id
this.load()
},
filters: filters,
filters,
methods: {
...mapActions(['delEvent']),
async load () {
@ -79,5 +84,23 @@ export default {
padding: 0px;
width: 100%;
}
#eventDetail .close_button:hover {
background-color: rgba(200, 100, 100, 0.4);
}
#eventDetail .card {
border: 0px;
}
#eventDetail .close_button {
background-color: rgba(100, 100, 100, 0.4);
color: red;
font-size: 20px;
border: none;
position: absolute;
top: 10px;
right: 10px;
}
</style>

View file

@ -3,18 +3,18 @@
p {{$t('export_intro')}}
li(v-if='filters.tags.length') {{$t('Tags')}}:
el-tag.ml-1(color='#409EFF' size='mini' v-for='tag in filters.tags') {{tag}}
el-tag.ml-1(color='#409EFF' size='mini' v-for='tag in filters.tags' :key='tag.tag') {{tag}}
li(v-if='filters.places.length') {{$t('Places')}}:
el-tag.ml-1(color='#409EFF' size='mini' v-for='place in filters.places') {{place}}
el-tag.ml-1(color='#409EFF' size='mini' v-for='place in filters.places' :key='place.id') {{place}}
el-tabs.mt-2(tabPosition='left' v-model='type')
el-tab-pane.pt-1(label='email' name='email')
p(v-html='$t(`export_email_explanation`)')
b-form
el-switch(v-model='mail.sendOnInsert' :active-text="$t('notify_on_insert')")
el-switch(v-model='reminder.send_on_insert' :active-text="$t('notify_on_insert')")
br
el-switch.mt-2(v-model='mail.reminder' :active-text="$t('send_reminder')")
el-input.mt-2(v-model='mail.mail' :placeholder="$t('Insert your address')")
el-switch.mt-2(v-model='reminder.send_reminder' :active-text="$t('send_reminder')")
el-input.mt-2(v-model='reminder.mail' :placeholder="$t('Insert your address')")
el-button.mt-2.float-right(type='success' @click='activate_email') {{$t('Send')}}
el-tab-pane.pt-1(label='feed rss' name='feed')
@ -31,7 +31,7 @@
p(v-html='$t(`export_list_explanation`)')
b-card.mb-1(no-body header='Eventi')
b-list-group#list(flush)
b-list-group-item.flex-column.align-items-start(v-for="event in filteredEvents"
b-list-group-item.flex-column.align-items-start(v-for="event in filteredEvents" :key='event.id'
:to='`/event/${event.id}`')
//- b-media
img(v-if='event.image_path' slot="aside" :src="imgPath(event)" alt="Meia Aside" style='max-height: 60px')
@ -39,7 +39,7 @@
strong.mb-1 {{event.title}}
br
small.float-right {{event.place.name}}
el-tag.mr-1(:color='tag.color' size='mini' v-for='tag in event.tags') {{tag.tag}}
el-tag.mr-1(:color='tag.color' size='mini' v-for='tag in event.tags' :key='tag.tag') {{tag.tag}}
el-input.mb-1(type='textarea' v-model='script')
el-button.float-right(plain type="primary" icon='el-icon-document' v-clipboard:copy="script") Copy
@ -65,7 +65,7 @@ export default {
return {
type: 'feed',
link: '',
mail: {},
reminder: { send_on_insert: true, send_reminder: false },
export_list: true,
script: `<iframe>Ti piacerebbe</iframe>`,
}
@ -97,7 +97,7 @@ export default {
}
}
return `${process.env.BASE_URL}/api/export/${this.type}${query}`
return `${process.env.VUE_APP_API}/api/export/${this.type}${query}`
},
imgPath (event) {
return event.image_path && event.image_path

View file

@ -1,13 +1,14 @@
<template lang="pug">
b-container
b-card-group(columns)
b-form-group.mt-1
div.mt-1
Search#search
Calendar
Event.item(v-for='event in filteredEvents'
:key='event.id'
:event='event')
</template>
<script>
import { mapState } from 'vuex'
import filters from '@/filters.js'

View file

@ -6,7 +6,7 @@
b-navbar-nav
b-nav-item(v-if='!logged' to='/login' v-b-tooltip :title='$t("Login")') <v-icon color='lightgreen' name='lock' />
span.d-md-none {{$t('User')}}
b-nav-item(v-if='logged' to='/new_event' v-b-tooltip :title='$t("Add Event")' ) <v-icon color='lightgreen' name='plus'/>
b-nav-item(to='/new_event' v-b-tooltip :title='$t("Add Event")' ) <v-icon color='lightgreen' name='plus'/>
span.d-md-none {{$t('Add Event')}}
b-nav-item(v-if='logged' to='/settings' v-b-tooltip :title='$t("Settings")') <v-icon color='orange' name='cog'/>
span.d-md-none {{$t('Settings')}}

View file

@ -1,7 +1,6 @@
<template lang='pug'>
b-modal(hide-header hide-footer
@hide='$router.replace("/")' :visible='true' @shown='$refs.email.focus()')
h4.text-center.center {{$t('Register')}}
b-modal(hide-footer
@hidden='$router.replace("/")' :title="$t('Register')" :visible='true' @shown='$refs.email.focus()')
b-form
p.text-muted(v-html="$t('register_explanation')")
b-input-group.mb-1
@ -46,7 +45,7 @@ export default {
this.$message({
message: this.$t('registration_complete'),
type: 'success'
});
})
} catch (e) {
console.error(e)
}

View file

@ -9,6 +9,7 @@
el-option(v-for='tag in tags' :key='tag.tag'
:label='tag.tag' :value='tag.tag')
</template>
<script>
import {mapState, mapActions} from 'vuex'

View file

@ -1,17 +1,17 @@
<template lang="pug">
b-card.column.pl-1(bg-variant='dark' text-variant='white' no-body)
b-card-header
strong Public events
b-btn.float-right(v-if='logged' variant='success' size='sm' to='/newEvent') <v-icon name="plus"/> Add Event
event(v-for='event in events', :event='event' :key='event.id')
</template>
<script>
import api from '@/api'
import event from './Event'
import { mapState } from 'vuex';
// <template lang="pug">
// b-card.column.pl-1(bg-variant='dark' text-variant='white' no-body)
// b-card-header
// strong Public events
// b-btn.float-right(v-if='logged' variant='success' size='sm' to='/newEvent') <v-icon name="plus"/> Add Event
// event(v-for='event in events', :event='event' :key='event.id')
// </template>
// <script>
// import api from '@/api'
// import event from './Event'
// import { mapState } from 'vuex';
export default {
components: {event},
computed: mapState(['events', 'logged'])
}
</script>
// export default {
// components: {event},
// computed: mapState(['events', 'logged'])
// }
// </script>

View file

@ -1,240 +0,0 @@
<template lang='pug'>
div(style="position: relative")
b-input-group
input.form-control(type="search"
ref="input"
:placeholder="placeholder"
v-model="search"
@input="update"
autocomplete="off"
@keydown.backspace="backspace"
@keydown.up.prevent="up"
@keydown.down.prevent="down"
@keydown.enter="hit"
@keydown.esc="reset(true)"
@blur="focus = false"
@focus="focus = true")
div
b-badge.mr-1(@click="removeSelected(sel)"
v-for="sel in selectedLabel"
:key="sel") <v-icon color='orange' name='times' /> {{sel}}
b-list-group.groupMenu(v-show='showDropdown')
b-list-group-item(:key="$index" v-for="(item, $index) in matched"
href='#'
:class="{'active': isActive($index)}"
@mousedown.prevent="hit"
@mousemove="setActive($index)")
slot(:name="templateName") {{textField ? item[textField] : item}}
</template>
<script>
export default {
props: {
value: {
twoWay : true,
type: [String, Array, Set],
default: ''
},
data: {
type: Array
},
template: {
type: String
},
templateName: {
type: String,
default: 'default'
},
valueField: {
type: String,
default: null
},
textField: {
type: String,
default: null
},
showClear: {
type: Boolean,
default: true
},
matchCase: {
type: Boolean,
default: false
},
matchStart: {
type: Boolean,
default: false
},
onHit: {
type: Function,
default () {
this.reset()
}
},
placeholder: {
type: String
},
updateOnMatchOnly: {
type: Boolean,
default: false,
},
multiple: {
type: Boolean,
default: false
},
maxMatch: {
type: Number,
default: 4
}
},
data () {
return {
focus: false,
noResults: true,
current: 0,
search: '',
selected: [],
}
},
watch: {
value(newValue) {
if (!newValue) {
this.search = '';
if (this.multiple) this.$emit('input', []) // this.selected = [];
} else {
if (!this.multiple) {
this.search = newValue
} else {
this.selected = newValue
}
}
}
},
computed: {
showDropdown () {
return this.focus
},
selectedValues () {
return this.selected.map(s => this.valueField ? (s[this.valueField] || s) : s);
},
selectedLabel () {
return this.selected.map(s => this.textField ? s[this.textField] || s: s);
},
matched () {
if (this.data) {
return this.data.filter(value => {
if(this.textField) value = value[this.textField];
if (this.multiple && this.selectedLabel.includes(value)) return false;
value = this.matchCase ? value : value.toLowerCase()
const query = this.matchCase ? this.search : this.search.toLowerCase()
return this.matchStart ? value.indexOf(query) === 0 : value.indexOf(query) !== -1
}).slice(0, this.maxMatch)
}
}
},
methods: {
update (e, value) {
if (this.multiple && this.search[this.search.length-1] === ',') {
this.search = this.search.substr(0, this.search.length-1)
this.hit(e)
return
}
if (!this.updateOnMatchOnly && !this.multiple) this.$emit('input', this.search);
this.focus = true;
if (!this.matched.length) {
this.focus = false;
this.current = 0
return;
}
// current selected item has to be in the match
if (this.matched.length <= this.current) {
this.current = this.matched.length-1;
}
},
backspace () {
if (this.search) return
this.selected.splice(-1, 1)
this.$emit('input', this.selected.length ? this.selectedValues : '');
},
reset (esc=false) {
this.search = '';
this.current = 0;
this.$refs.input.focus();
if (esc) {
this.focus = false
} else {
this.selected = [];
this.$emit('input', '');
}
},
setActive (index) {
this.current = index
},
isActive (index) {
return this.current === index
},
removeSelected (label) {
this.selected = this.selected.filter( s => (this.textField ? s[this.textField] || s: s) !== label);
this.$emit('input', this.selected.length ? this.selectedValues : []);
},
// click or enter on curren item
hit (e) {
e.preventDefault();
let code = '';
let item = '';
if (this.matched.length !== 0 && this.focus) {
item = this.matched[this.current];
code = this.textField ? item[this.textField] : item;
// code = this.valueField ? item[this.valueField] : item;
} else {
code = this.search;
}
if (this.multiple) {
if (code) {
this.selected.push(code);
this.search = '';
this.$emit('input', this.selected);
this.focus = false;
// this.update();
}
} else {
this.$emit('input', code);
this.current = 0;
this.focus = false;
this.search = code
}
this.$emit('enter')
},
// manage up/down arrow key
up () {
if (this.current > 0) this.current--
},
down () {
if (this.current < this.matched.length - 1) this.current++
}
},
}
</script>
<style scoped>
.groupMenu {
position: absolute;
top: 40px;
width: 100%;
z-index: 2;
}
.badge {
cursor: pointer;
}
</style>

View file

@ -1,5 +1,4 @@
<template lang="pug">
//- el-dialog(@close='$router.replace("/")' :title="edit?$t('Edit event'):$t('New event')" center :close-on-press-escape='false' :visible='true')
b-modal(@hidden='$router.replace("/")' :title="edit?$t('Edit event'):$t('New event')" size='md' :visible='true' hide-footer)
b-container
el-tabs.mb-2(v-model='activeTab' v-loading='sending')
@ -10,7 +9,7 @@
el-form(label-width='120px')
el-form-item(:label='$t("Where")')
el-select(v-model='event.place.name' @change='placeChoosed' filterable allow-create default-first-option)
el-option(v-for='place in places_name' :label='place' :value='place')
el-option(v-for='place in places_name' :label='place' :value='place' :key='place.id')
el-form-item(:label='$t("Address")')
el-input(ref='address' v-model='event.place.address' @keydown.native.enter='next')
el-button.float-right(@click='next' :disabled='!couldProceed') {{$t('Next')}}
@ -39,10 +38,9 @@
el-input.mb-3(v-model='event.description' type='textarea' :rows='3')
span {{$t('tag_explanation')}}
br
//- typeahead(v-model="event.tags" :data='tags' multiple)
el-select(v-model='event.tags' multiple filterable allow-create
default-first-option placeholder='Tag')
el-option(v-for='tag in tags' :key='tag'
el-option(v-for='tag in tags' :key='tag.tag'
:label='tag' :value='tag')
el-button.float-right(@click='next' :disabled='!couldProceed') {{$t('Next')}}
@ -80,6 +78,12 @@ export default {
}
},
name: 'newEvent',
watch: {
'time.start' (value) {
let [h, m] = value.split(':')
this.time.end = (Number(h)+1) + ':' + m
}
},
async mounted () {
if (this.$route.params.id) {
this.id = this.$route.params.id

View file

@ -53,18 +53,25 @@ const it = {
Places: 'Luoghi',
Tags: 'Etichette',
Name: 'Nome',
Preview: 'Visualizza',
Save: 'Salva',
Address: 'Indirizzo',
Remove: 'Elimina',
Password: 'Password',
Email: 'Email',
User: 'Utente',
Confirm: 'Conferma',
Events: 'Eventi',
Color: 'Colore',
Edit: 'Modifica',
Admin: 'Amministra',
Today: 'Oggi',
Export: 'Esporta',
send_reminder: 'Ricordamelo il giorno prima',
event_confirmed: 'Evento confermato!',
notify_on_insert: `Notifica all'inserimento`,
'event_confirm_explanation': 'Puoi approvare gli eventi inseriti da utenti non registrati',
'admin_place_explanation': 'Puoi modificare i luoghi inseriti',
'Edit event': 'Modifica evento',
'New event': 'Nuovo evento',
'Insert your address': 'Inserisci il tuo indirizzo',

View file

@ -5,13 +5,12 @@ import VCalendar from 'v-calendar'
import 'vue-awesome/icons'
import Icon from 'vue-awesome/components/Icon'
import Typeahead from '@/components/Typeahead'
import VueClipboard from 'vue-clipboard2'
import 'v-calendar/lib/v-calendar.min.css'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
// import 'bootstrap-vue/dist/bootstrap-vue.css'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
@ -35,7 +34,6 @@ Vue.use(VCalendar, {
Vue.use(BootstrapVue)
Vue.use(VueI18n)
Vue.use(VueClipboard)
Vue.component('typeahead', Typeahead)
Vue.component('v-icon', Icon)
const messages = {

View file

@ -113,7 +113,9 @@ export default new Vuex.Store({
},
async addEvent ({ commit }, formData) {
const event = await api.addEvent(formData)
commit('addEvent', event)
if (this.state.logged) {
commit('addEvent', event)
}
},
async updateEvent ({ commit }, formData) {
const event = await api.updateEvent(formData)

View file

@ -1,4 +1,9 @@
process.env.VUE_APP_API = process.env.NODE_ENV === 'production' ? process.env.BASE_URL || 'http://localhost:9000' : 'http://localhost:9000'
process.env.VUE_APP_TITLE = process.env.TITLE || 'Gancio'
process.env.VUE_APP_DESCRIPTION = process.env.DESCRIPTION || 'Event manager for radical movements'
module.exports = {
publicPath: process.env.BASE_URL,
devServer: {
disableHostCheck: true
},

View file

@ -64,16 +64,17 @@
"@babel/traverse" "^7.1.0"
"@babel/types" "^7.0.0"
"@babel/helper-create-class-features-plugin@^7.2.3":
version "7.2.3"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.2.3.tgz#f6e719abb90cb7f4a69591e35fd5eb89047c4a7c"
integrity sha512-xO/3Gn+2C7/eOUeb0VRnSP1+yvWHNxlpAot1eMhtoKDCN7POsyQP5excuT5UsV5daHxMWBeIIOeI5cmB8vMRgQ==
"@babel/helper-create-class-features-plugin@^7.3.0", "@babel/helper-create-class-features-plugin@^7.3.4":
version "7.3.4"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.3.4.tgz#092711a7a3ad8ea34de3e541644c2ce6af1f6f0c"
integrity sha512-uFpzw6L2omjibjxa8VGZsJUPL5wJH0zzGKpoz0ccBkzIa6C8kWNUbiBmQ0rgOKWlHJ6qzmfa6lTiGchiV8SC+g==
dependencies:
"@babel/helper-function-name" "^7.1.0"
"@babel/helper-member-expression-to-functions" "^7.0.0"
"@babel/helper-optimise-call-expression" "^7.0.0"
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/helper-replace-supers" "^7.2.3"
"@babel/helper-replace-supers" "^7.3.4"
"@babel/helper-split-export-declaration" "^7.0.0"
"@babel/helper-define-map@^7.1.0":
version "7.1.0"

View file

@ -16,6 +16,8 @@ services:
build: .
ports:
- '12300:12300'
volumes:
- ./uploads:/usr/src/app/uploads
env_file: .env
environment:

View file

@ -21,6 +21,7 @@
"pg": "^7.8.1",
"pug": "^2.0.3",
"sequelize": "^4.41.0",
"sharp": "^0.21.3",
"sqlite3": "^4.0.3"
},
"devDependencies": {

View file

@ -3,7 +3,7 @@ rss(version='2.0', xmlns:atom='<a href="http://www.w3.org/2005/Atom" rel="nofoll
channel
title #{config.title}
link <a href="#{config.baseurl}" rel="nofollow">#{config.baseurl}</a>
atom:link(href='<a href="#{config.apiurl}/export/feed/rss" rel="nofollow">#{config.apiurl}/export/feed/rss</a>', rel='self', type='application/rss+xml')
<atom:link href="#{config.apiurl}/export/feed/rss" rel='self' type='application/rss+xml' />
description #{config.description}
language #{config.locale}
//- if events.length
@ -17,6 +17,7 @@ rss(version='2.0', xmlns:atom='<a href="http://www.w3.org/2005/Atom" rel="nofoll
| <h4>#{event.title}</h4>
| <strong>#{event.place.name} - #{event.place.address}</strong>
| #{moment(event.start_datetime).format("ddd, D MMMM HH:mm")}<br/>
| <img src="#{config.apiurl}/#{event.image_path}"/>
| <pre>!{event.description}</pre>
| ]]>
pubDate= new Date(event.createdAt).toUTCString()