front end, config
This commit is contained in:
3
.eslintrc.js
Normal file
3
.eslintrc.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
"extends": "standard"
|
||||||
|
};
|
||||||
@@ -31,12 +31,10 @@ api.route('/user/event')
|
|||||||
api.route('/user/event/:id')
|
api.route('/user/event/:id')
|
||||||
.delete(isAuth, userController.delEvent)
|
.delete(isAuth, userController.delEvent)
|
||||||
|
|
||||||
|
api.get('/event/meta', eventController.getMeta)
|
||||||
api.route('/event/:event_id')
|
api.route('/event/:event_id')
|
||||||
.get(eventController.get)
|
.get(eventController.get)
|
||||||
|
|
||||||
api.route('/event/meta')
|
|
||||||
.get(eventController.getMeta)
|
|
||||||
|
|
||||||
|
|
||||||
api.get('/export/feed', exportController.feed)
|
api.get('/export/feed', exportController.feed)
|
||||||
api.get('/export/ics', exportController.ics)
|
api.get('/export/ics', exportController.ics)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const Auth = {
|
|||||||
jwt.verify(token, config.secret, async (err, decoded) => {
|
jwt.verify(token, config.secret, async (err, decoded) => {
|
||||||
if (err) return res.status(403).send({ message: 'Failed to authenticate token ' + err })
|
if (err) return res.status(403).send({ message: 'Failed to authenticate token ' + err })
|
||||||
console.log('DECODED TOKEN', decoded)
|
console.log('DECODED TOKEN', decoded)
|
||||||
req.user = await User.findOne({ where: {email: decoded.email}})
|
req.user = await User.findOne({ where: { email: decoded.email } })
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
const env = process.env.NODE_ENV
|
const env = process.env.NODE_ENV
|
||||||
module.exports = require('../config/config.' + env + '.js')
|
const conf = require('../config/config.' + env + '.json')
|
||||||
|
module.exports = conf
|
||||||
|
|||||||
@@ -9,34 +9,33 @@ const moment = require('moment')
|
|||||||
moment.locale('it')
|
moment.locale('it')
|
||||||
const botController = {
|
const botController = {
|
||||||
bots: [],
|
bots: [],
|
||||||
async initialize() {
|
async initialize () {
|
||||||
console.log('initialize bots')
|
console.log('initialize bots')
|
||||||
const botUsers = await User.findAll({where: { mastodon_auth: { [Op.ne]: null }}})
|
const botUsers = await User.findAll({ where: { mastodon_auth: { [Op.ne]: null } } })
|
||||||
console.log(botUsers)
|
console.log(botUsers)
|
||||||
botController.bots = botUsers.map(user => {
|
botController.bots = botUsers.map(user => {
|
||||||
console.log('initialize bot ', user.name)
|
console.log('initialize bot ', user.name)
|
||||||
console.log('.. ', user.mastodon_auth)
|
console.log('.. ', user.mastodon_auth)
|
||||||
const { client_id, client_secret, access_token } = user.mastodon_auth
|
const { client_id, client_secret, access_token } = user.mastodon_auth
|
||||||
const bot = new Mastodon({ access_token, api_url: `https://${user.instance}/api/v1/` })
|
const bot = new Mastodon({ access_token, api_url: `https://${user.mastodon_instance}/api/v1/` })
|
||||||
console.log(bot)
|
|
||||||
const listener = bot.stream('streaming/direct')
|
const listener = bot.stream('streaming/direct')
|
||||||
listener.on('message', botController.message)
|
listener.on('message', botController.message)
|
||||||
listener.on('error', botController.error)
|
listener.on('error', botController.error)
|
||||||
return {email: user.email, bot}
|
return { email: user.email, bot }
|
||||||
})
|
})
|
||||||
console.log(botController.bots)
|
console.log(botController.bots)
|
||||||
},
|
},
|
||||||
add (user) {
|
add (user) {
|
||||||
const bot = new Mastodon({ access_token: user.mastodon_auth.access_token, api_url: `https://${user.instance}/api/v1/` })
|
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')
|
const listener = bot.stream('streaming/direct')
|
||||||
listener.on('message', botController.message)
|
listener.on('message', botController.message)
|
||||||
listener.on('error', botController.error)
|
listener.on('error', botController.error)
|
||||||
botController.bots.push({ email: user.email, bot})
|
botController.bots.push({ email: user.email, bot })
|
||||||
},
|
},
|
||||||
post(user, event) {
|
post (user, event) {
|
||||||
const { bot } = botController.bots.filter(b => b.email === user.email)[0]
|
const { bot } = botController.bots.filter(b => b.email === user.email)[0]
|
||||||
const status = `${event.title} @ ${event.place.name} ${moment(event.start_datetime).format('ddd, D MMMM HH:mm')} -
|
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}`
|
${event.description} - ${event.tags.map(t => '#' + t.tag).join(' ')} ${config.baseurl}/event/${event.id}`
|
||||||
return bot.post('/statuses', { status, visibility: 'private' })
|
return bot.post('/statuses', { status, visibility: 'private' })
|
||||||
},
|
},
|
||||||
async message (msg) {
|
async message (msg) {
|
||||||
@@ -44,17 +43,17 @@ ${event.description} - ${event.tags.map(t => '#'+t.tag).join(' ')} ${config.base
|
|||||||
console.log(msg.data.accounts)
|
console.log(msg.data.accounts)
|
||||||
const replyid = msg.data.in_reply_to_id || msg.data.last_status.in_reply_to_id
|
const replyid = msg.data.in_reply_to_id || msg.data.last_status.in_reply_to_id
|
||||||
if (!replyid) return
|
if (!replyid) return
|
||||||
const event = await Event.findOne( {where: {activitypub_id: replyid}} )
|
// const event = await Event.findOne({ where: { activitypub_id: replyid } })
|
||||||
if (!event) {
|
// if (!event) {
|
||||||
// check for comment..
|
// check for comment..
|
||||||
const comment = await Comment.findOne( {where: { }})
|
// 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 })
|
// 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)
|
// event.addComment(comment)
|
||||||
console.log(event)
|
// console.log(event)
|
||||||
// const comment = await Comment.findOne( { where: {activitypub_id: msg.data.in_reply_to}} )
|
// const comment = await Comment.findOne( { where: {activitypub_id: msg.data.in_reply_to}} )
|
||||||
// console.log('dentro message ', data)
|
// console.log('dentro message ', data)
|
||||||
return
|
|
||||||
// add comment to specified event
|
// add comment to specified event
|
||||||
// let comment
|
// let comment
|
||||||
//if (!event) {
|
//if (!event) {
|
||||||
@@ -69,6 +68,5 @@ ${event.description} - ${event.tags.map(t => '#'+t.tag).join(' ')} ${config.base
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setTimeout(botController.initialize, 2000)
|
setTimeout(botController.initialize, 2000)
|
||||||
module.exports = botController
|
module.exports = botController
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
const jwt = require('jsonwebtoken')
|
|
||||||
const { User, Event, Comment, Tag, Place } = require('../model')
|
const { User, Event, Comment, Tag, Place } = require('../model')
|
||||||
const config = require('../config')
|
|
||||||
const mail = require('../mail')
|
|
||||||
const moment = require('moment')
|
const moment = require('moment')
|
||||||
const Sequelize = require('sequelize')
|
const Sequelize = require('sequelize')
|
||||||
|
|
||||||
@@ -9,9 +6,9 @@ const eventController = {
|
|||||||
|
|
||||||
async addComment (req, res) {
|
async addComment (req, res) {
|
||||||
// comment could be added to an event or to another comment
|
// comment could be added to an event or to another comment
|
||||||
let event = await Event.findOne({where: {activitypub_id: req.body.id}})
|
let event = await Event.findOne({ where: { activitypub_id: req.body.id } })
|
||||||
if (!event) {
|
if (!event) {
|
||||||
const comment = await Comment.findOne({where: {activitypub_id: req.body.id}, include: Event})
|
const comment = await Comment.findOne({ where: { activitypub_id: req.body.id }, include: Event })
|
||||||
event = comment.event
|
event = comment.event
|
||||||
}
|
}
|
||||||
const comment = new Comment(req.body)
|
const comment = new Comment(req.body)
|
||||||
@@ -19,16 +16,11 @@ const eventController = {
|
|||||||
res.json(comment)
|
res.json(comment)
|
||||||
},
|
},
|
||||||
|
|
||||||
// async boost (req, res) {
|
async getMeta (req, res) {
|
||||||
// const event = await Event.findById(req.body.id)
|
console.log('GET META')
|
||||||
// req.user.addBoost(event)
|
|
||||||
// res.status(200)
|
|
||||||
// },
|
|
||||||
|
|
||||||
async getMeta(req, res) {
|
|
||||||
const places = await Place.findAll()
|
const places = await Place.findAll()
|
||||||
const tags = await Tag.findAll()
|
const tags = await Tag.findAll()
|
||||||
res.json({tags, places})
|
res.json({ tags, places })
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateTag (req, res) {
|
async updateTag (req, res) {
|
||||||
@@ -41,17 +33,15 @@ const eventController = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async get(req, res) {
|
async get (req, res) {
|
||||||
const id = req.params.event_id
|
const id = req.params.event_id
|
||||||
const event = await Event.findByPk(id, { include: [User, Tag, Comment, Place]})
|
const event = await Event.findByPk(id, { include: [User, Tag, Comment, Place] })
|
||||||
res.json(event)
|
res.json(event)
|
||||||
},
|
},
|
||||||
|
|
||||||
async getAll (req, res) {
|
async getAll (req, res) {
|
||||||
const start = moment().year(req.params.year).month(req.params.month).startOf('month').subtract(1, 'week')
|
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 end = moment().year(req.params.year).month(req.params.month).endOf('month').add(1, 'week')
|
||||||
console.log('start', start)
|
|
||||||
console.log('end', end)
|
|
||||||
const events = await Event.findAll({
|
const events = await Event.findAll({
|
||||||
where: {
|
where: {
|
||||||
[Sequelize.Op.and]: [
|
[Sequelize.Op.and]: [
|
||||||
@@ -63,7 +53,7 @@ const eventController = {
|
|||||||
include: [User, Comment, Tag, Place]
|
include: [User, Comment, Tag, Place]
|
||||||
})
|
})
|
||||||
res.json(events)
|
res.json(events)
|
||||||
},
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ const bot = require('./bot')
|
|||||||
const userController = {
|
const userController = {
|
||||||
async login (req, res) {
|
async login (req, res) {
|
||||||
// find the user
|
// find the user
|
||||||
const user = await User.findOne({where: { email: req.body.email }})
|
const user = await User.findOne({ where: { email: req.body.email } })
|
||||||
if (!user) {
|
if (!user) {
|
||||||
res.status(404).json({ success: false, message: 'AUTH_FAIL' })
|
res.status(404).json({ success: false, message: 'AUTH_FAIL' })
|
||||||
} else if (user) {
|
} else if (user) {
|
||||||
if (!user.is_active) {
|
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
|
// check if password matches
|
||||||
else if (!await user.comparePassword(req.body.password)) {
|
else if (!await user.comparePassword(req.body.password)) {
|
||||||
@@ -42,10 +42,9 @@ const userController = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async delEvent (req, res) {
|
async delEvent (req, res) {
|
||||||
//check if event is mine
|
// check if event is mine
|
||||||
const event = await Event.findByPk(req.params.id)
|
const event = await Event.findByPk(req.params.id)
|
||||||
if (event && (req.user.is_admin || req.user.id === event.userId))
|
if (event && (req.user.is_admin || req.user.id === event.userId)) {
|
||||||
{
|
|
||||||
await event.destroy()
|
await event.destroy()
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
} else {
|
} else {
|
||||||
@@ -67,13 +66,13 @@ const userController = {
|
|||||||
eventDetails.image_path = req.file.path
|
eventDetails.image_path = req.file.path
|
||||||
}
|
}
|
||||||
|
|
||||||
//create place
|
// create place
|
||||||
let place
|
let place
|
||||||
try {
|
try {
|
||||||
place = await Place.findOrCreate({where: {name: body.place_name},
|
place = await Place.findOrCreate({ where: { name: body.place_name },
|
||||||
defaults: {address: body.place_address }})
|
defaults: { address: body.place_address } })
|
||||||
.spread((place, created) => place)
|
.spread((place, created) => place)
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
}
|
}
|
||||||
let event = await Event.create(eventDetails)
|
let event = await Event.create(eventDetails)
|
||||||
@@ -82,15 +81,17 @@ const userController = {
|
|||||||
// create/assign tags
|
// create/assign tags
|
||||||
console.log(body.tags)
|
console.log(body.tags)
|
||||||
if (body.tags) {
|
if (body.tags) {
|
||||||
await Tag.bulkCreate(body.tags.map(t => ({ tag: t})), {ignoreDuplicates: true})
|
await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true })
|
||||||
const tags = await Tag.findAll({where: { tag: body.tags }})
|
const tags = await Tag.findAll({ where: { tag: body.tags } })
|
||||||
await event.addTags(tags)
|
await event.addTags(tags)
|
||||||
}
|
}
|
||||||
await req.user.addEvent(event)
|
await req.user.addEvent(event)
|
||||||
event = await Event.findByPk(event.id, {include: [User, Tag, Place]})
|
event = await Event.findByPk(event.id, { include: [User, Tag, Place] })
|
||||||
// check if bot exists
|
// check if bot exists
|
||||||
if (req.user.mastodon_auth) {
|
if (req.user.mastodon_auth) {
|
||||||
const post = await bot.post(req.user, event)
|
const post = await bot.post(req.user, event)
|
||||||
|
event.activitypub_id = post.id
|
||||||
|
event.save()
|
||||||
}
|
}
|
||||||
return res.json(event)
|
return res.json(event)
|
||||||
},
|
},
|
||||||
@@ -101,24 +102,26 @@ const userController = {
|
|||||||
await event.update(body)
|
await event.update(body)
|
||||||
let place
|
let place
|
||||||
try {
|
try {
|
||||||
place = await Place.findOrCreate({where: {name: body.place_name},
|
place = await Place.findOrCreate({ where: { name: body.place_name },
|
||||||
defaults: {address: body.place_address }})
|
defaults: { address: body.place_address } })
|
||||||
.spread((place, created) => place)
|
.spread((place, created) => place)
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
console.log('catch', e)
|
console.log('error', e)
|
||||||
}
|
}
|
||||||
await event.setPlace(place)
|
await event.setPlace(place)
|
||||||
await event.setTags([])
|
await event.setTags([])
|
||||||
console.log(body.tags)
|
console.log(body.tags)
|
||||||
if (body.tags) {
|
if (body.tags) {
|
||||||
await Tag.bulkCreate(body.tags.map(t => ({ tag: t})), {ignoreDuplicates: true})
|
await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true })
|
||||||
const tags = await Tag.findAll({where: { tag: body.tags }})
|
const tags = await Tag.findAll({ where: { tag: body.tags } })
|
||||||
await event.addTags(tags)
|
await event.addTags(tags)
|
||||||
}
|
}
|
||||||
const newEvent = await Event.findByPk(event.id, {include: [User, Tag, Place]})
|
const newEvent = await Event.findByPk(event.id, { include: [User, Tag, Place] })
|
||||||
// check if bot exists
|
// check if bot exists
|
||||||
if (req.user.mastodon_auth) {
|
if (req.user.mastodon_auth) {
|
||||||
const post = await bot.post(req.user, newEvent)
|
const post = await bot.post(req.user, newEvent)
|
||||||
|
event.activitypub_id = post.id
|
||||||
|
await event.save()
|
||||||
}
|
}
|
||||||
return res.json(newEvent)
|
return res.json(newEvent)
|
||||||
},
|
},
|
||||||
@@ -133,7 +136,7 @@ const userController = {
|
|||||||
const { client_id, client_secret } = await Mastodon.createOAuthApp(`https://${instance}/api/v1/apps`, 'eventi', 'read write', `${config.baseurl}/settings`)
|
const { client_id, client_secret } = await Mastodon.createOAuthApp(`https://${instance}/api/v1/apps`, 'eventi', 'read write', `${config.baseurl}/settings`)
|
||||||
const url = await Mastodon.getAuthorizationUrl(client_id, client_secret, `https://${instance}`, 'read write', `${config.baseurl}/settings`)
|
const url = await Mastodon.getAuthorizationUrl(client_id, client_secret, `https://${instance}`, 'read write', `${config.baseurl}/settings`)
|
||||||
console.log(req.user)
|
console.log(req.user)
|
||||||
req.user.instance = instance
|
req.user.mastodon_instance = instance
|
||||||
req.user.mastodon_auth = { client_id, client_secret }
|
req.user.mastodon_auth = { client_id, client_secret }
|
||||||
await req.user.save()
|
await req.user.save()
|
||||||
res.json(url)
|
res.json(url)
|
||||||
@@ -142,13 +145,13 @@ const userController = {
|
|||||||
async code (req, res) {
|
async code (req, res) {
|
||||||
const code = req.body.code
|
const code = req.body.code
|
||||||
const { client_id, client_secret } = req.user.mastodon_auth
|
const { client_id, client_secret } = req.user.mastodon_auth
|
||||||
const instance = req.user.instance
|
const instance = req.user.mastodon_instance
|
||||||
try {
|
try {
|
||||||
const token = await Mastodon.getAccessToken(client_id, client_secret, code, `https://${instance}`, '${config.baseurl}/settings')
|
const token = await Mastodon.getAccessToken(client_id, client_secret, code, `https://${instance}`, `${config.baseurl}/settings`)
|
||||||
const mastodon_auth = { client_id, client_secret, access_token: token}
|
const mastodon_auth = { client_id, client_secret, access_token: token }
|
||||||
req.user.mastodon_auth = mastodon_auth
|
req.user.mastodon_auth = mastodon_auth
|
||||||
await req.user.save()
|
await req.user.save()
|
||||||
await botController.add(token)
|
await bot.add(token)
|
||||||
res.json(req.user)
|
res.json(req.user)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.json(e)
|
res.json(e)
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
const Sequelize = require('sequelize')
|
const Sequelize = require('sequelize')
|
||||||
const env = process.env.NODE_ENV || 'development'
|
const env = process.env.NODE_ENV || 'development'
|
||||||
const conf = require('../config/config.' + env + '.js')
|
const conf = require('../config/config.' + env + '.json')
|
||||||
const db = new Sequelize(conf.db)
|
const db = new Sequelize(conf.db)
|
||||||
|
|
||||||
|
// db.sync({ force: true })
|
||||||
db.sync({ force: true })
|
|
||||||
// db.sync()
|
// db.sync()
|
||||||
|
|
||||||
module.exports = db
|
module.exports = db
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const Email = require('email-templates')
|
const Email = require('email-templates')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const config = require('./config');
|
const config = require('./config')
|
||||||
|
|
||||||
const mail = {
|
const mail = {
|
||||||
send (addresses, template, locals) {
|
send (addresses, template, locals) {
|
||||||
|
|||||||
@@ -6,28 +6,28 @@ const Event = db.define('event', {
|
|||||||
title: Sequelize.STRING,
|
title: Sequelize.STRING,
|
||||||
description: Sequelize.STRING,
|
description: Sequelize.STRING,
|
||||||
multidate: Sequelize.BOOLEAN,
|
multidate: Sequelize.BOOLEAN,
|
||||||
start_datetime: { type: Sequelize.DATE, index: true},
|
start_datetime: { type: Sequelize.DATE, index: true },
|
||||||
end_datetime: {type: Sequelize.DATE, index: true},
|
end_datetime: { type: Sequelize.DATE, index: true },
|
||||||
image_path: Sequelize.STRING,
|
image_path: Sequelize.STRING,
|
||||||
activitypub_id: { type: Sequelize.INTEGER, index: true },
|
activitypub_id: { type: Sequelize.INTEGER, index: true }
|
||||||
})
|
})
|
||||||
|
|
||||||
const Tag = db.define('tag', {
|
const Tag = db.define('tag', {
|
||||||
tag: { type: Sequelize.STRING, index: true, unique: true, primaryKey: true},
|
tag: { type: Sequelize.STRING, index: true, unique: true, primaryKey: true },
|
||||||
color: { type: Sequelize.STRING }
|
color: { type: Sequelize.STRING }
|
||||||
})
|
})
|
||||||
|
|
||||||
const Comment = db.define('comment', {
|
const Comment = db.define('comment', {
|
||||||
activitypub_id: { type: Sequelize.INTEGER, index: true },
|
activitypub_id: { type: Sequelize.INTEGER, index: true },
|
||||||
author: Sequelize.STRING,
|
author: Sequelize.STRING,
|
||||||
text: Sequelize.STRING,
|
text: Sequelize.STRING
|
||||||
})
|
})
|
||||||
|
|
||||||
const MailSubscription = db.define('subscription' , {
|
const MailSubscription = db.define('subscription', {
|
||||||
filters: Sequelize.JSON,
|
filters: Sequelize.JSON,
|
||||||
mail: Sequelize.TEXT,
|
mail: Sequelize.TEXT,
|
||||||
send_on_add: Sequelize.BOOLEAN,
|
send_on_add: Sequelize.BOOLEAN,
|
||||||
send_reminder: Sequelize.INTEGER,
|
send_reminder: Sequelize.INTEGER
|
||||||
})
|
})
|
||||||
|
|
||||||
const Place = db.define('place', {
|
const Place = db.define('place', {
|
||||||
@@ -38,15 +38,13 @@ const Place = db.define('place', {
|
|||||||
Comment.belongsTo(Event)
|
Comment.belongsTo(Event)
|
||||||
Event.hasMany(Comment)
|
Event.hasMany(Comment)
|
||||||
|
|
||||||
Event.belongsToMany(Tag, {through: 'tagEvent'})
|
Event.belongsToMany(Tag, { through: 'tagEvent' })
|
||||||
Tag.belongsToMany(Event, {through: 'tagEvent'})
|
Tag.belongsToMany(Event, { through: 'tagEvent' })
|
||||||
|
|
||||||
Event.belongsToMany(User, {through: 'boost'})
|
|
||||||
Event.belongsTo(User)
|
Event.belongsTo(User)
|
||||||
Event.belongsTo(Place)
|
Event.belongsTo(Place)
|
||||||
|
|
||||||
User.hasMany(Event)
|
User.hasMany(Event)
|
||||||
Place.hasMany(Event)
|
Place.hasMany(Event)
|
||||||
User.belongsToMany(User, {through: 'userFollower', as: 'follower'})
|
|
||||||
|
|
||||||
module.exports = { Event, Comment, Tag, Place }
|
module.exports = { Event, Comment, Tag, Place, MailSubscription }
|
||||||
|
|||||||
@@ -5,17 +5,18 @@ const Sequelize = require('sequelize')
|
|||||||
const User = db.define('user', {
|
const User = db.define('user', {
|
||||||
email: {
|
email: {
|
||||||
type: Sequelize.STRING,
|
type: Sequelize.STRING,
|
||||||
unique: {msg: 'Email already exists'},
|
unique: { msg: 'Email already exists' },
|
||||||
index: true, allowNull: false },
|
index: true,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
description: Sequelize.TEXT,
|
description: Sequelize.TEXT,
|
||||||
password: Sequelize.STRING,
|
password: Sequelize.STRING,
|
||||||
is_admin: Sequelize.BOOLEAN,
|
is_admin: Sequelize.BOOLEAN,
|
||||||
is_active: Sequelize.BOOLEAN,
|
is_active: Sequelize.BOOLEAN,
|
||||||
instance: Sequelize.STRING,
|
mastodon_instance: Sequelize.STRING,
|
||||||
mastodon_auth: Sequelize.JSON
|
mastodon_auth: Sequelize.JSON
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
User.prototype.comparePassword = async function (pwd) {
|
User.prototype.comparePassword = async function (pwd) {
|
||||||
if (!this.password) return false
|
if (!this.password) return false
|
||||||
const ret = await bcrypt.compare(pwd, this.password)
|
const ret = await bcrypt.compare(pwd, this.password)
|
||||||
|
|||||||
3
client/.env
Normal file
3
client/.env
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
VUE_BASE_URL=https://localhost:8080/
|
||||||
|
VUE_INSTANCE_API=https://localhost:9000/
|
||||||
|
VUE_APP_TITLE=Eventi
|
||||||
3
client/.eslintrc.js
Normal file
3
client/.eslintrc.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
"extends": "standard"
|
||||||
|
};
|
||||||
21
client/README.md
Normal file
21
client/README.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Gancio
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
```
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
yarn run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
yarn run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
```
|
||||||
|
yarn run lint
|
||||||
|
```
|
||||||
5
client/babel.config.js
Normal file
5
client/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/app'
|
||||||
|
]
|
||||||
|
}
|
||||||
13
client/config.js
Normal file
13
client/config.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const env = process.env.NODE_ENV
|
||||||
|
const beConf = require('../config/config.' + env + '.json')
|
||||||
|
|
||||||
|
const conf = {
|
||||||
|
// environment
|
||||||
|
title: beConf.title,
|
||||||
|
|
||||||
|
// base url
|
||||||
|
baseurl: beConf.baseurl,
|
||||||
|
apiurl: beConf.apiurl
|
||||||
|
}
|
||||||
|
|
||||||
|
export default conf
|
||||||
1
client/dist/css/app.1265c9f1.css
vendored
Normal file
1
client/dist/css/app.1265c9f1.css
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.groupMenu[data-v-526ddfcd]{position:absolute;top:40px;width:100%;z-index:2}.badge[data-v-526ddfcd]{cursor:pointer}#tabss ul{-webkit-box-pack:space-evenly;-ms-flex-pack:space-evenly;justify-content:space-evenly;background:linear-gradient(#fff,#fff 22px,#007bff,#fff 23px,#fff)}#tabss ul .nav-link{background-color:#fff}#tabss ul .nav-link.active{background-color:#007bff}#eventDetail .modal-body{padding:0}#eventDetail .card{margin-left:-5px}#eventDetail .badge{margin-left:.1rem}.card-columns .card[data-v-f79fd7b2]{margin-top:.3em;margin-bottom:0}.card-img[data-v-f79fd7b2]{max-height:180px;-o-object-fit:cover;object-fit:cover}.badge[data-v-f79fd7b2]{margin-left:.1rem}#calendar{margin-bottom:0;margin-top:.3em}.card-columns{-webkit-column-count:1;column-count:1;-webkit-column-gap:.3em;column-gap:.3em}@media (min-width:576px){.container{max-width:none}.card-columns{-webkit-column-count:2;column-count:2;-webkit-column-gap:.3em;column-gap:.3em}}@media (min-width:950px){.container{max-width:1400px}.card-columns{-webkit-column-count:3;column-count:3;-webkit-column-gap:.3em;column-gap:.3em}}.item{-webkit-transition:all .2s;transition:all .2s;display:inline-block;width:100%}.list-enter,.list-leave-to{opacity:0;-webkit-transform:translateY(30px);transform:translateY(30px)}.list-leave-active{position:absolute;top:0;width:0;left:0;height:0;z-index:-10}#footer{position:absolute;width:100%;bottom:0}#search,#search ul{-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}body,html{scrollbar-face-color:#313543;scrollbar-track-color:rgba(0,0,0,.1);font-family:Lato,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:1.1em;color:#2c3e50;background:#000}::-webkit-scrollbar{width:7px;height:7px}::-webkit-scrollbar-thumb{background:#313543;border:0 none #fff;border-radius:6px}::-webkit-scrollbar-thumb:hover{background:#353a49}::-webkit-scrollbar-thumb:active{background:#313543}::-webkit-scrollbar-track{border:0 none #fff;border-radius:6px;background:rgba(0,0,0,.1)}::-webkit-scrollbar-track:active,::-webkit-scrollbar-track:hover{background:#282c37}::-webkit-scrollbar-corner{background:transparent}
|
||||||
7
client/dist/css/chunk-vendors.e4346a46.css
vendored
Normal file
7
client/dist/css/chunk-vendors.e4346a46.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
client/dist/favicon.ico
vendored
Normal file
BIN
client/dist/favicon.ico
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
client/dist/fonts/element-icons.2fad952a.woff
vendored
Normal file
BIN
client/dist/fonts/element-icons.2fad952a.woff
vendored
Normal file
Binary file not shown.
BIN
client/dist/fonts/element-icons.6f0a7632.ttf
vendored
Normal file
BIN
client/dist/fonts/element-icons.6f0a7632.ttf
vendored
Normal file
Binary file not shown.
1
client/dist/index.html
vendored
Normal file
1
client/dist/index.html
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.ico><title>eventi</title><link href=/css/app.1265c9f1.css rel=preload as=style><link href=/css/chunk-vendors.e4346a46.css rel=preload as=style><link href=/js/app.85eb2b80.js rel=preload as=script><link href=/js/chunk-vendors.f50f5004.js rel=preload as=script><link href=/css/chunk-vendors.e4346a46.css rel=stylesheet><link href=/css/app.1265c9f1.css rel=stylesheet></head><body><noscript><strong>We're sorry but eventi doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/js/chunk-vendors.f50f5004.js></script><script src=/js/app.85eb2b80.js></script></body></html>
|
||||||
2
client/dist/js/app.85eb2b80.js
vendored
Normal file
2
client/dist/js/app.85eb2b80.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
client/dist/js/app.85eb2b80.js.map
vendored
Normal file
1
client/dist/js/app.85eb2b80.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
109
client/dist/js/chunk-vendors.f50f5004.js
vendored
Normal file
109
client/dist/js/chunk-vendors.f50f5004.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
client/dist/js/chunk-vendors.f50f5004.js.map
vendored
Normal file
1
client/dist/js/chunk-vendors.f50f5004.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
66
client/package.json
Normal file
66
client/package.json
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"name": "eventi",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.18.0",
|
||||||
|
"bootstrap-vue": "^2.0.0-rc.11",
|
||||||
|
"element-ui": "^2.5.4",
|
||||||
|
"mastodon-api": "^1.3.0",
|
||||||
|
"moment": "^2.23.0",
|
||||||
|
"node-sass": "^4.11.0",
|
||||||
|
"npm": "^6.8.0",
|
||||||
|
"postcss-flexbugs-fixes": "^4.1.0",
|
||||||
|
"pug": "^2.0.3",
|
||||||
|
"sass-loader": "^7.1.0",
|
||||||
|
"v-calendar": "^0.9.7",
|
||||||
|
"vue": "^2.5.17",
|
||||||
|
"vue-awesome": "^3.3.1",
|
||||||
|
"vue-clipboard2": "^0.3.0",
|
||||||
|
"vue-i18n": "^8.5.0",
|
||||||
|
"vue-router": "^3.0.1",
|
||||||
|
"vuex": "^3.0.1",
|
||||||
|
"vuex-persist": "^2.0.0",
|
||||||
|
"weekstart": "^1.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-plugin-babel": "^3.2.0",
|
||||||
|
"@vue/cli-plugin-eslint": "^3.2.0",
|
||||||
|
"@vue/cli-service": "^3.2.0",
|
||||||
|
"@vue/eslint-config-standard": "^4.0.0",
|
||||||
|
"babel-eslint": "^10.0.1",
|
||||||
|
"eslint": "^5.8.0",
|
||||||
|
"eslint-plugin-vue": "^5.0.0-0",
|
||||||
|
"pug-plain-loader": "^1.0.0",
|
||||||
|
"vue-template-compiler": "^2.5.17"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:vue/essential",
|
||||||
|
"@vue/standard"
|
||||||
|
],
|
||||||
|
"rules": {},
|
||||||
|
"parserOptions": {
|
||||||
|
"parser": "babel-eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"postcss": {
|
||||||
|
"plugins": {
|
||||||
|
"autoprefixer": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not ie <= 8"
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
client/public/favicon.ico
Normal file
BIN
client/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
17
client/public/index.html
Normal file
17
client/public/index.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<title>Gancio</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but eventi doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
139
client/src/App.vue
Normal file
139
client/src/App.vue
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<template lang='pug'>
|
||||||
|
#app
|
||||||
|
b-navbar(type="dark" variant="dark" toggleable='lg')
|
||||||
|
b-navbar-brand(to='/') Gancio
|
||||||
|
b-navbar-toggle(target='nav_collapse')
|
||||||
|
b-collapse#nav_collapse(is-nav)
|
||||||
|
b-navbar-nav.ml-auto(v-if='logged')
|
||||||
|
b-nav-item(to='/new_event') <v-icon color='lightgreen' name='plus'/> {{$t('Add Event')}}
|
||||||
|
b-nav-item(@click='search=!search') <v-icon color='lightgreen' name='search'/> {{$t('Search')}}
|
||||||
|
b-nav-item(to='/settings') <v-icon color='orange' name='cog'/> {{$t('Settings')}}
|
||||||
|
b-nav-item(v-if='user.is_admin' to='/admin') <v-icon color='lightblue' name='tools'/> {{$t('Admin')}}
|
||||||
|
b-nav-item(variant='danger' @click='logout') <v-icon color='red' name='sign-out-alt'/> {{$t('Logout')}}
|
||||||
|
b-navbar-nav.ml-auto(v-else)
|
||||||
|
b-nav-item(@click='search=!search') <v-icon color='lightgreen' name='search'/> {{$t('Search')}}
|
||||||
|
b-nav-item(to='/register') {{$t('Register')}}
|
||||||
|
b-nav-item(to='/login') {{$t('Login')}}
|
||||||
|
transition(name='toggle')
|
||||||
|
b-navbar#search(v-if='search' type='dark' variant="dark" toggleable='lg')
|
||||||
|
b-navbar-nav
|
||||||
|
b-nav-form
|
||||||
|
typeahead.ml-1(v-model='filters_places'
|
||||||
|
textField='name' valueField='name'
|
||||||
|
updateOnMatchOnly
|
||||||
|
:data='places' multiple placeholder='Luogo')
|
||||||
|
b-nav-form
|
||||||
|
typeahead.ml-1(v-model='filters_tags'
|
||||||
|
updateOnMatchOnly
|
||||||
|
textField='tag' valueField='tag'
|
||||||
|
:data='tags' multiple placeholder='Tags')
|
||||||
|
b-navbar-nav.ml-auto(variant='dark')
|
||||||
|
b-nav-item(to='/export/feed' href='#') <v-icon color='orange' name='rss'/> feed
|
||||||
|
b-nav-item(to='/export/ics') <v-icon color='orange' name='calendar'/> cal
|
||||||
|
b-nav-item(to='/export/email') <v-icon color='orange' name='envelope'/> mail
|
||||||
|
b-nav-item(to='/export/embed') <v-icon color='orange' name='code'/> embed
|
||||||
|
b-nav-item(to='/export/print') <v-icon color='orange' name='print'/> print
|
||||||
|
Home
|
||||||
|
transition(name="fade" mode="out-in")
|
||||||
|
router-view(name='modal')
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import moment from 'moment'
|
||||||
|
import api from '@/api'
|
||||||
|
import { mapActions, mapState } from 'vuex';
|
||||||
|
import Register from '@/components/Register'
|
||||||
|
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'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
mounted () {
|
||||||
|
this.updateMeta()
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {search: false}
|
||||||
|
},
|
||||||
|
components: { Register, Login, Home, Settings, newEvent, eventDetail },
|
||||||
|
computed: {
|
||||||
|
...mapState(['logged', 'user', 'filters', 'tags', 'places']),
|
||||||
|
filters_tags: {
|
||||||
|
set (value) {
|
||||||
|
console.log('dentro set ', value)
|
||||||
|
this.setSearchTags(value)
|
||||||
|
},
|
||||||
|
get () {
|
||||||
|
console.log('dentro get')
|
||||||
|
console.log(this.filters)
|
||||||
|
return this.filters.tags
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filters_places: {
|
||||||
|
set (value) {
|
||||||
|
this.setSearchPlaces(value)
|
||||||
|
},
|
||||||
|
get () {
|
||||||
|
return this.filters.places
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: mapActions(['logout', 'updateMeta', 'addSearchTag',
|
||||||
|
'setSearchTags', 'setSearchPlaces', 'addSearchPlace']),
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#footer {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search,
|
||||||
|
#search ul {
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
scrollbar-face-color: #313543;
|
||||||
|
scrollbar-track-color: rgba(0, 0, 0, 0.1);
|
||||||
|
font-family: Lato,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: #2c3e50;
|
||||||
|
background: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px; }
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #313543;
|
||||||
|
border: 0px none #ffffff;
|
||||||
|
border-radius: 6px; }
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #353a49; }
|
||||||
|
::-webkit-scrollbar-thumb:active {
|
||||||
|
background: #313543; }
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
border: 0px none #ffffff;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(0, 0, 0, 0.1); }
|
||||||
|
::-webkit-scrollbar-track:hover {
|
||||||
|
background: #282c37; }
|
||||||
|
::-webkit-scrollbar-track:active {
|
||||||
|
background: #282c37; }
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background: transparent; }
|
||||||
|
|
||||||
|
|
||||||
|
/* .column {
|
||||||
|
margin-top: 3px;
|
||||||
|
margin-right: 3px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
width: 350px;
|
||||||
|
} */
|
||||||
|
|
||||||
|
</style>
|
||||||
47
client/src/api.js
Normal file
47
client/src/api.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import config from '../config'
|
||||||
|
import store from './store'
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: config.apiurl,
|
||||||
|
withCredentials: false,
|
||||||
|
responseType: 'json',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function get (path) {
|
||||||
|
return api.get(path, { headers: { 'x-access-token': store.state.token } }).then(ret => ret.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
function post (path, data) {
|
||||||
|
return api.post(path, data, { headers: { 'x-access-token': store.state.token } }).then(ret => ret.data)
|
||||||
|
}
|
||||||
|
function put (path, data) {
|
||||||
|
return api.put(path, data, { headers: { 'x-access-token': store.state.token } }).then(ret => ret.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
function del (path) {
|
||||||
|
console.log(store.state.token)
|
||||||
|
return api.delete(path, { headers: { 'x-access-token': store.state.token } }).then(ret => ret.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
login: (email, password) => post('/login', { email, password }),
|
||||||
|
register: user => post('/user', user),
|
||||||
|
getAllEvents: (month, year) => get(`/event/${year}/${month}/`),
|
||||||
|
addEvent: event => post('/user/event', event),
|
||||||
|
updateEvent: event => put('/user/event', event),
|
||||||
|
delEvent: eventId => del(`/user/event/${eventId}`),
|
||||||
|
getEvent: eventId => get(`/event/${eventId}`),
|
||||||
|
getMeta: () => get('/event/meta'),
|
||||||
|
getUser: () => get('/user'),
|
||||||
|
getUsers: () => get('/users'),
|
||||||
|
updateTag: (tag) => put('/tag', tag),
|
||||||
|
updateUser: user => put('/user', user),
|
||||||
|
getAuthURL: mastodonInstance => post('/user/getauthurl', mastodonInstance),
|
||||||
|
setCode: code => post('/user/code', code),
|
||||||
|
getKnowLocations: () => get('/locations'),
|
||||||
|
getKnowTags: () => get('/tags')
|
||||||
|
}
|
||||||
5
client/src/assets/main.css
Normal file
5
client/src/assets/main.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/* html, body {
|
||||||
|
max-height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
} */
|
||||||
|
|
||||||
67
client/src/components/Admin.vue
Normal file
67
client/src/components/Admin.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
b-modal(hide-footer hide-header
|
||||||
|
@hide='$router.go(-1)' size='lg' :visible='true')
|
||||||
|
h4.text-center Admin
|
||||||
|
b-tabs(pills)
|
||||||
|
b-tab
|
||||||
|
template(slot='title')
|
||||||
|
v-icon(name='users')
|
||||||
|
span {{$t('Users')}}
|
||||||
|
b-table(:items='users' :fields='userFields' striped hover)
|
||||||
|
template(slot='action' slot-scope='data')
|
||||||
|
b-button.mr-1(:variant='data.item.is_active?"warning":"success"' @click='toggle(data.item)') {{data.item.is_active?$t('Deactivate'):$t('Activate')}}
|
||||||
|
b-button(:variant='data.item.is_admin?"danger":"warning"' @click='toggleAdmin(data.item)') {{data.item.is_admin?$t('Remove Admin'):$t('Admin')}}
|
||||||
|
b-tab
|
||||||
|
template(slot='title')
|
||||||
|
v-icon(name='map-marker-alt')
|
||||||
|
span {{$t('Places')}}
|
||||||
|
b-table(:items='places' :fields='placeFields' striped hover)
|
||||||
|
b-tab
|
||||||
|
template(slot='title')
|
||||||
|
v-icon(name='tag')
|
||||||
|
span {{$t('Tags')}}
|
||||||
|
b-table(:items='tags' :fields='tagFields' striped hover)
|
||||||
|
template(slot='tag' slot-scope='data')
|
||||||
|
b-badge(:style='{backgroundColor: data.item.color}') {{data.item.tag}}
|
||||||
|
template(slot='color' slot-scope='data')
|
||||||
|
el-color-picker(v-model='data.item.color' @change='updateColor(data.item)')
|
||||||
|
b-tab
|
||||||
|
template(slot='title')
|
||||||
|
v-icon(name='tools')
|
||||||
|
span {{$t('Settings')}}
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
import api from '@/api'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Admin',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
users: [],
|
||||||
|
userFields: ['email', 'action'],
|
||||||
|
placeFields: ['name', 'address'],
|
||||||
|
tagFields: ['tag', 'color'],
|
||||||
|
description: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted () {
|
||||||
|
this.users = await api.getUsers()
|
||||||
|
},
|
||||||
|
computed: mapState(['tags', 'places']),
|
||||||
|
methods: {
|
||||||
|
async toggle(user) {
|
||||||
|
user.is_active = !user.is_active
|
||||||
|
const newuser = await api.updateUser(user)
|
||||||
|
},
|
||||||
|
async toggleAdmin(user) {
|
||||||
|
user.is_admin = !user.is_admin
|
||||||
|
const newuser = await api.updateUser(user)
|
||||||
|
},
|
||||||
|
async updateColor(tag) {
|
||||||
|
const newTag = await api.updateTag(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
82
client/src/components/Calendar.vue
Normal file
82
client/src/components/Calendar.vue
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
v-calendar#calendar.card(
|
||||||
|
:attributes='attributes'
|
||||||
|
:from-page.sync='page'
|
||||||
|
is-expanded is-inline)
|
||||||
|
div(slot='popover', slot-scope='{ customData }')
|
||||||
|
router-link(:to="`/event/${customData.id}`") {{customData.start_datetime|hour}} - {{customData.title}} @{{customData.place.name}}
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapState, mapActions } from 'vuex'
|
||||||
|
import filters from '@/filters'
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Calendar',
|
||||||
|
filters,
|
||||||
|
data () {
|
||||||
|
const month = moment().month()+1
|
||||||
|
const year = moment().year()
|
||||||
|
return {
|
||||||
|
page: { month, year},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.updateEvents(this.page)
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
page () {
|
||||||
|
this.updateEvents(this.page)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['updateEvents']),
|
||||||
|
eventToAttribute(event) {
|
||||||
|
let e = {
|
||||||
|
key: event.id,
|
||||||
|
customData: event,
|
||||||
|
order: event.start_datetime,
|
||||||
|
popover: {
|
||||||
|
slot: 'popover',
|
||||||
|
visibility: 'hover'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = event.tags.length && event.tags[0].color ? event.tags[0].color : 'rgba(200,200,200,0.5)'
|
||||||
|
if (event.multidate) {
|
||||||
|
e.dates = {
|
||||||
|
start: event.start_datetime, end: event.end_datetime
|
||||||
|
}
|
||||||
|
e.highlight = { backgroundColor: color,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
borderWidth: '4px' }
|
||||||
|
} else {
|
||||||
|
e.dates = event.start_datetime
|
||||||
|
e.dot = { backgroundColor: color, borderColor: color, borderWidth: '3px' }
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(['events', 'tags']),
|
||||||
|
attributes () {
|
||||||
|
return [
|
||||||
|
{ key: 'todaly', dates: new Date(),
|
||||||
|
highlight: {
|
||||||
|
backgroundColor: '#aaffaa'
|
||||||
|
},
|
||||||
|
popover: {label: this.$t('Today')}
|
||||||
|
},
|
||||||
|
...this.events.map(this.eventToAttribute)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#calendar {
|
||||||
|
margin-bottom: 0em;
|
||||||
|
margin-top: 0.3em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
60
client/src/components/Event.vue
Normal file
60
client/src/components/Event.vue
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
b-card(bg-variant='dark' text-variant='white'
|
||||||
|
@click='$router.push("/event/" + event.id)'
|
||||||
|
:img-src='imgPath')
|
||||||
|
h4 {{event.title}}
|
||||||
|
div <v-icon name='clock'/> {{event.start_datetime|datetime}}
|
||||||
|
span(v-b-popover.hover="event.place && event.place.address || ''") <v-icon name='map-marker-alt'/> {{event.place.name}}
|
||||||
|
br
|
||||||
|
b-badge(:style='{backgroundColor: tag.color}' v-for='tag in event.tags' href='#'
|
||||||
|
@click.stop='addSearchTag(tag)') {{tag.tag}}
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapState, mapActions } from 'vuex';
|
||||||
|
import api from '@/api'
|
||||||
|
import filters from '@/filters'
|
||||||
|
import config from '../../config'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['event'],
|
||||||
|
computed: {
|
||||||
|
...mapState(['user']),
|
||||||
|
imgPath () {
|
||||||
|
return this.event.image_path && config.apiurl + '/../' + this.event.image_path
|
||||||
|
},
|
||||||
|
mine () {
|
||||||
|
return this.event.userId === this.user.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filters,
|
||||||
|
methods: {
|
||||||
|
...mapActions(['delEvent', 'addSearchTag']),
|
||||||
|
async remove () {
|
||||||
|
await api.delEvent(this.event.id)
|
||||||
|
this.delEvent(this.event.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
/* .card::before {
|
||||||
|
border-top: 4px solid black;
|
||||||
|
content: ''
|
||||||
|
} */
|
||||||
|
|
||||||
|
.card-columns .card {
|
||||||
|
margin-top: 0.3em;
|
||||||
|
margin-bottom: 0em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-img {
|
||||||
|
max-height: 180px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
margin-left: 0.1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
91
client/src/components/EventDetail.vue
Normal file
91
client/src/components/EventDetail.vue
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
b-modal#eventDetail(hide-footer hide-header
|
||||||
|
@hide='$router.go(-1)' size='lg' :visible='true')
|
||||||
|
|
||||||
|
b-card(bg-variant='dark' href='#' text-variant='white'
|
||||||
|
no-body, :img-src='imgPath')
|
||||||
|
b-card-header
|
||||||
|
h3 {{event.title}}
|
||||||
|
v-icon(name='clock')
|
||||||
|
span {{event.start_datetime|datetime}}
|
||||||
|
br
|
||||||
|
v-icon(name='map-marker-alt')
|
||||||
|
span {{event.place.name}} - {{event.place.address}}
|
||||||
|
br
|
||||||
|
b-card-footer(v-if='event.description || event.tags') {{event.description}}
|
||||||
|
br
|
||||||
|
b-badge(:style='{backgroundColor: tag.color}' v-for='tag in event.tags') {{tag.tag}}
|
||||||
|
b-navbar(v-if='mine' type="dark" variant="dark" toggleable='lg')
|
||||||
|
b-navbar-nav.ml-auto
|
||||||
|
b-nav-item(@click.prevent='remove') <v-icon color='red' name='times'/> {{$t('Remove')}}
|
||||||
|
b-nav-item(:to='"/edit/"+event.id') <v-icon color='orange' name='edit'/> {{$t('Edit')}}
|
||||||
|
//- b-card-footer.text-right
|
||||||
|
//- span.mr-3 {{event.comments.length}} <v-icon name='comments'/>
|
||||||
|
//- a(href='#', @click='remove')
|
||||||
|
v-icon(color='orange' name='times')
|
||||||
|
//- b-card-footer(v-for='comment in event.comments')
|
||||||
|
strong {{comment.author}}
|
||||||
|
div(v-html='comment.text')
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapState, mapActions } from 'vuex';
|
||||||
|
import api from '@/api'
|
||||||
|
import filters from '@/filters'
|
||||||
|
import config from '../../config'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
...mapState(['user']),
|
||||||
|
imgPath () {
|
||||||
|
return this.event.image_path && config.apiurl + '/../' + this.event.image_path
|
||||||
|
},
|
||||||
|
mine () {
|
||||||
|
return this.event.userId === this.user.id || this.user.is_admin
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
event: { comments: [], place: {}},
|
||||||
|
id: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.id = this.$route.params.id
|
||||||
|
this.load()
|
||||||
|
},
|
||||||
|
filters: filters,
|
||||||
|
methods: {
|
||||||
|
...mapActions(['delEvent']),
|
||||||
|
async load () {
|
||||||
|
const event = await api.getEvent(this.id)
|
||||||
|
this.event = event
|
||||||
|
},
|
||||||
|
async remove () {
|
||||||
|
await api.delEvent(this.event.id)
|
||||||
|
this.delEvent(this.event.id)
|
||||||
|
this.$router.go(-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
#eventDetail .modal-body {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .card::before {
|
||||||
|
border-top: 4px solid black;
|
||||||
|
content: ''
|
||||||
|
} */
|
||||||
|
#eventDetail .card {
|
||||||
|
margin-left: -5px;
|
||||||
|
}
|
||||||
|
/* #eventDetail .card-img {
|
||||||
|
max-height: 150px;
|
||||||
|
object-fit: cover;
|
||||||
|
} */
|
||||||
|
#eventDetail .badge {
|
||||||
|
margin-left: 0.1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
60
client/src/components/Export.vue
Normal file
60
client/src/components/Export.vue
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
b-modal(hide-footer hide-header
|
||||||
|
@hide='$router.go(-1)' size='lg' :visible='true' v-if='type')
|
||||||
|
h3.text-center Export {{type}}
|
||||||
|
b-input-group.mb-2(v-if='showLink')
|
||||||
|
b-form-input( v-model='link' autocomplete='off')
|
||||||
|
b-input-group-append
|
||||||
|
b-button(variant="success" v-clipboard:copy="link") <v-icon name='clipboard'/> Copy
|
||||||
|
p {{$t('export_intro')}}
|
||||||
|
p(v-html='$t(`export_${type}_explanation`)')
|
||||||
|
li(v-if='filters.tags.length') {{$t('Tags')}} ->
|
||||||
|
b-badge.ml-1(v-for='tag in filters.tags') {{tag}}
|
||||||
|
li(v-if='filters.places.length') {{$t('Places')}}
|
||||||
|
b-badge.ml-1(v-for='place in filters.places') {{place}}
|
||||||
|
b-form(v-if="type==='email'")
|
||||||
|
el-switch(v-model='mail.sendOnInsert' :active-text="$t('notify_on_insert')")
|
||||||
|
br
|
||||||
|
el-switch(v-model='mail.reminder' :active-text="$t('send_reminder')")
|
||||||
|
b-form-input.mt-1(v-model='mail.mail' :placeholder="$t('Insert your address')")
|
||||||
|
b-button.mt-1.float-right(variant='success' @click='activate_email') {{$t('Send')}}
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
import config from '../../config'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Export',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
type: '',
|
||||||
|
link: '',
|
||||||
|
mail: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.type = this.$route.params.type
|
||||||
|
this.link = this.loadLink()
|
||||||
|
if (this.type === 'email' && this.logged) {
|
||||||
|
this.mail.mail = this.user.email
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
activate_email () {
|
||||||
|
this.$router.go(-1)
|
||||||
|
},
|
||||||
|
loadLink () {
|
||||||
|
const filters = this.filters.tags.join(',')
|
||||||
|
return `${config.apiurl}/export/${this.type}/${filters}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(['filters', 'user', 'logged']),
|
||||||
|
showLink () {
|
||||||
|
return (['feed', 'ics'].indexOf(this.type)>-1)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
81
client/src/components/Home.vue
Normal file
81
client/src/components/Home.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
b-container
|
||||||
|
b-card-group(columns)
|
||||||
|
Calendar
|
||||||
|
//- transition-group(name="list" tag="div")
|
||||||
|
Event.item(v-for='event in filteredEvents'
|
||||||
|
:key='event.id'
|
||||||
|
:event='event')
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
import filters from '@/filters.js'
|
||||||
|
import Event from '@/components/Event'
|
||||||
|
import Calendar from '@/components/Calendar'
|
||||||
|
import {intersection} from 'lodash'
|
||||||
|
export default {
|
||||||
|
name: 'Home',
|
||||||
|
components: { Event, Calendar },
|
||||||
|
computed: {
|
||||||
|
...mapState(['events', 'filters']),
|
||||||
|
filteredEvents () {
|
||||||
|
if (!this.filters.tags.length && !this.filters.places.length) return this.events
|
||||||
|
return this.events.filter(e => {
|
||||||
|
if (this.filters.tags.length) {
|
||||||
|
const m = intersection(e.tags.map(t => t.tag), this.filters.tags)
|
||||||
|
if (m.length>0) return true
|
||||||
|
}
|
||||||
|
if (this.filters.places.length) {
|
||||||
|
if (this.filters.places.find(p => p === e.place.name))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
|
||||||
|
.card-columns {
|
||||||
|
column-count: 1;
|
||||||
|
column-gap: 0.3em;
|
||||||
|
}
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
.container {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
.card-columns {
|
||||||
|
column-count: 2;
|
||||||
|
column-gap: 0.3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 950px) {
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
}
|
||||||
|
.card-columns {
|
||||||
|
column-count: 3;
|
||||||
|
column-gap: 0.3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
transition: all .2s;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.list-enter, .list-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
.list-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
width: 0px;
|
||||||
|
left: 0px;
|
||||||
|
height: 0px;
|
||||||
|
z-index: -10;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
47
client/src/components/Login.vue
Normal file
47
client/src/components/Login.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<template lang='pug'>
|
||||||
|
b-modal(hide-header hide-footer @shown="$refs.email.focus()"
|
||||||
|
@hide='$router.go(-1)' :visible='true')
|
||||||
|
h4.text-center.center {{$t('Login')}}
|
||||||
|
b-form
|
||||||
|
//- p.text-muted Sign In to your account
|
||||||
|
b-input-group.mb-1
|
||||||
|
b-input-group-prepend
|
||||||
|
b-input-group-text
|
||||||
|
v-icon(name="user")
|
||||||
|
b-form-input(ref='email' v-model="email" type="text" class="form-control" placeholder="E-mail" autocomplete="email")
|
||||||
|
b-input-group.mb-3
|
||||||
|
b-input-group-prepend
|
||||||
|
b-input-group-text
|
||||||
|
v-icon(name="lock")
|
||||||
|
b-form-input(v-model="password" type="password" class="form-control" placeholder="Password" autocomplete="current-password")
|
||||||
|
b-button.float-right(variant="success" @click='submit') Login
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import api from '@/api'
|
||||||
|
import { mapActions } from 'vuex'
|
||||||
|
import { log } from 'util';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Login',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
password: '',
|
||||||
|
email: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['login']),
|
||||||
|
async submit (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const user = await api.login(this.email, this.password)
|
||||||
|
if (!user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.login(user)
|
||||||
|
this.email = this.password = ''
|
||||||
|
this.$router.go(-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
58
client/src/components/Register.vue
Normal file
58
client/src/components/Register.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<template lang='pug'>
|
||||||
|
b-modal(hide-header hide-footer
|
||||||
|
@hide='$router.go(-1)' :visible='true' @shown='$refs.email.focus()')
|
||||||
|
h4.text-center.center {{$t('Register')}}
|
||||||
|
b-form
|
||||||
|
p.text-muted(v-html="$t('register_explanation')")
|
||||||
|
b-input-group.mb-1
|
||||||
|
b-input-group-prepend
|
||||||
|
b-input-group-text @
|
||||||
|
b-form-input(ref='email' v-model='user.email' type="text" class="form-control" placeholder="Email" autocomplete="email" )
|
||||||
|
|
||||||
|
b-input-group.mb-1
|
||||||
|
b-input-group-prepend
|
||||||
|
b-input-group-text
|
||||||
|
v-icon(name='lock')
|
||||||
|
b-form-input(v-model='user.password' type="password" class="form-control" placeholder="Password")
|
||||||
|
|
||||||
|
b-input-group.mb-1
|
||||||
|
b-input-group-prepend
|
||||||
|
b-input-group-text
|
||||||
|
v-icon(name='envelope-open-text')
|
||||||
|
b-form-textarea(v-model='user.description' type="text" rows='3' class="form-control" :placeholder="$t('Description')")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
b-button.float-right(variant="success" @click='register') {{$t('Send')}}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import api from '@/api'
|
||||||
|
import { mapActions } from 'vuex';
|
||||||
|
export default {
|
||||||
|
name: 'Register',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
error: {},
|
||||||
|
user: { }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['login']),
|
||||||
|
async register () {
|
||||||
|
try {
|
||||||
|
const user = await api.register(this.user)
|
||||||
|
// this.login(user)
|
||||||
|
// this.user = { name: '' }
|
||||||
|
this.$router.go(-1)
|
||||||
|
this.$message({
|
||||||
|
message: this.$t('registration_complete'),
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
44
client/src/components/Settings.vue
Normal file
44
client/src/components/Settings.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
b-modal(hide-header hide-footer @hide='$router.push("/")' :visible='true')
|
||||||
|
h4.text-center {{$t('Settings')}}
|
||||||
|
b-form
|
||||||
|
b-input-group.mt-1(prepend='Email')
|
||||||
|
b-form-input(v-model="user.email")
|
||||||
|
//- b-form-checkbox(v-model="tmpUser.user.autoboost") Autoboost
|
||||||
|
b-input-group.mt-1(prepend='Mastodon instance')
|
||||||
|
b-form-input(v-model="mastodon_instance")
|
||||||
|
b-input-group-append
|
||||||
|
b-button(@click='associate', variant='primary') Associate
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapState, mapActions } from 'vuex'
|
||||||
|
import api from '@/api'
|
||||||
|
export default {
|
||||||
|
props: ['code'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
mastodon_instance: '',
|
||||||
|
user: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: mapState(['oauth']),
|
||||||
|
async mounted () {
|
||||||
|
const code = this.$route.query.code
|
||||||
|
if (code) {
|
||||||
|
const res = await api.setCode({code})
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await api.getUser()
|
||||||
|
this.user = user
|
||||||
|
this.mastodon_instance = user.instance
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async associate () {
|
||||||
|
const url = await api.getAuthURL({instance: this.mastodon_instance})
|
||||||
|
setTimeout( () => window.location.href=url, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
17
client/src/components/Timeline.vue
Normal file
17
client/src/components/Timeline.vue
Normal file
@@ -0,0 +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';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {event},
|
||||||
|
computed: mapState(['events', 'logged'])
|
||||||
|
}
|
||||||
|
</script>
|
||||||
240
client/src/components/Typeahead.vue
Normal file
240
client/src/components/Typeahead.vue
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
<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") {{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) {
|
||||||
|
if (this.multiple && e.data === ',') {
|
||||||
|
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>
|
||||||
9
client/src/components/index.js
Normal file
9
client/src/components/index.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import Register from '@/components/Register'
|
||||||
|
import Login from '@/components/Login'
|
||||||
|
import Settings from '@/components/Settings'
|
||||||
|
import newEvent from '@/components/newEvent'
|
||||||
|
import eventDetail from '@/components/EventDetail'
|
||||||
|
import Home from '@/components/Home'
|
||||||
|
import Event from '@/components/event'
|
||||||
|
|
||||||
|
module.exports = { Home, eventDetail, newEvent, Settings, Login, Register, Event }
|
||||||
206
client/src/components/newEvent.vue
Normal file
206
client/src/components/newEvent.vue
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
b-modal(hide-header hide-footer no-close-on-backdrop
|
||||||
|
@hide='$router.go(-1)' no-close-on-esc size='lg' :visible='true')
|
||||||
|
h4.text-center.center {{edit?$t('Edit event'):$t('New event')}}
|
||||||
|
b-tabs#tabss(pills v-model='activeTab')
|
||||||
|
b-form
|
||||||
|
b-tab
|
||||||
|
template(slot='title')
|
||||||
|
v-icon(name='map-marker-alt')
|
||||||
|
span {{$t('Where')}}
|
||||||
|
b-card-body
|
||||||
|
span.text-muted {{$t('where_explanation')}}
|
||||||
|
typeahead.mb-3(v-model='event.place.name' :data='places_name' @enter='placeChoosed')
|
||||||
|
span.text-muted {{$t('address_explanation')}}
|
||||||
|
b-form-input(ref='address' v-model='event.place.address' @keydown.native.enter='next')
|
||||||
|
b-tab
|
||||||
|
template(slot='title')
|
||||||
|
v-icon(name='clock')
|
||||||
|
span {{$t('When')}}
|
||||||
|
b-card-body
|
||||||
|
el-switch.float-right(v-model='event.multidate' :active-text="$t('multidate_explanation')")
|
||||||
|
span.text-muted {{event.multidate ? $t('dates_explanation') : $t('date_explanation')}}
|
||||||
|
v-date-picker.mb-3(:mode='event.multidate ? "range" : "single"' v-model='date' is-inline
|
||||||
|
is-expanded :min-date='new Date()' @input='date ? $refs.time_start.focus() : false')
|
||||||
|
b-row
|
||||||
|
b-col
|
||||||
|
label.text-muted {{$t('time_start_explanation')}}
|
||||||
|
el-time-select(ref='time_start'
|
||||||
|
v-model="time.start"
|
||||||
|
:picker-options="{ start: '00:00', step: '00:30', end: '24:00'}")
|
||||||
|
b-col.text-right
|
||||||
|
label.text-muted {{$t('time_end_explanation')}}
|
||||||
|
el-time-select(
|
||||||
|
v-model='time.end'
|
||||||
|
:picker-options="{start: `${event.multidate?'00:00':time.start}`, step: '00:30', end: '24:00'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
b-tab
|
||||||
|
template(slot='title')
|
||||||
|
v-icon(name='file-alt')
|
||||||
|
span {{$t('What')}}
|
||||||
|
b-card-body
|
||||||
|
span.text-muted {{$t('what_explanation')}}
|
||||||
|
b-form-input.mb-3(v-model.trim='event.title' autocomplete='off')
|
||||||
|
span.text-muted {{$t('description_explanation')}}
|
||||||
|
b-form-textarea.mb-3(v-model='event.description' :rows='3')
|
||||||
|
span.text-muted {{$t('tag_explanation')}}
|
||||||
|
typeahead(v-model="event.tags" :data='tags' multiple)
|
||||||
|
b-tab
|
||||||
|
template(slot='title')
|
||||||
|
v-icon(name='image')
|
||||||
|
span {{$t('Media')}}
|
||||||
|
b-card-body
|
||||||
|
span.text-muted {{$t('media_explanation')}}
|
||||||
|
b-form-file(v-model='event.image', :placeholder='$t("Poster")' accept='image/*')
|
||||||
|
b-button(v-if='activeTab==0' variant='danger' @click='$router.go(-1)') {{$t('Cancel')}}
|
||||||
|
b-button.float-left(v-else variant='danger' @click='prev') {{$t('Prev')}}
|
||||||
|
b-button.float-right(v-if='activeTab<3' variant='success' @click='next' :disabled='!couldProceed') {{$t('Next')}}
|
||||||
|
b-button.float-right(v-else variant='success' @click='done') {{edit?$t('Edit'):$t('Send')}}
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import api from '@/api'
|
||||||
|
import { mapActions, mapState } from 'vuex'
|
||||||
|
import moment from 'moment'
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
event: {
|
||||||
|
place: { name: '', address: '' },
|
||||||
|
title: '', description: '', tags: [],
|
||||||
|
multidate: false,
|
||||||
|
},
|
||||||
|
id: null,
|
||||||
|
activeTab: 0,
|
||||||
|
date: null,
|
||||||
|
time: { start: '00:00', end: null },
|
||||||
|
edit: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
name: 'newEvent',
|
||||||
|
async mounted () {
|
||||||
|
if (this.$route.params.id) {
|
||||||
|
this.id = this.$route.params.id
|
||||||
|
this.edit = true
|
||||||
|
const event = await api.getEvent(this.id)
|
||||||
|
// this.event.place = {name: event.place.name, address: event.place.address }
|
||||||
|
this.event.place.name = event.place.name
|
||||||
|
this.event.place.address = event.place.address || ''
|
||||||
|
this.event.multidate = event.multidate
|
||||||
|
this.date = event.start_datetime
|
||||||
|
this.time.start = moment(event.start_datetime).format('HH:mm')
|
||||||
|
this.time.end = moment(event.end_datetime).format('HH:mm')
|
||||||
|
this.event.title = event.title
|
||||||
|
this.event.description = event.description
|
||||||
|
this.event.id = event.id
|
||||||
|
if (event.tags) {
|
||||||
|
this.event.tags = event.tags.map(t => t.tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
this.updateMeta()
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
tags: state => state.tags.map(t => t.tag ),
|
||||||
|
places_name: state => state.places.map(p => p.name ),
|
||||||
|
places: state => state.places
|
||||||
|
}),
|
||||||
|
couldProceed () {
|
||||||
|
switch(this.activeTab) {
|
||||||
|
case 0:
|
||||||
|
return this.event.place.name.length>0 &&
|
||||||
|
this.event.place.address.length>0
|
||||||
|
case 1:
|
||||||
|
return true
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
return this.event.title.length>0
|
||||||
|
break
|
||||||
|
case 3:
|
||||||
|
return true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['addEvent', 'updateEvent', 'updateMeta']),
|
||||||
|
next () {
|
||||||
|
this.activeTab++
|
||||||
|
},
|
||||||
|
prev () {
|
||||||
|
this.activeTab--
|
||||||
|
},
|
||||||
|
placeChoosed () {
|
||||||
|
const place = this.places.find( p => p.name === this.event.place.name )
|
||||||
|
if (place && place.address) {
|
||||||
|
this.event.place.address = place.address
|
||||||
|
}
|
||||||
|
this.$refs.address.focus()
|
||||||
|
},
|
||||||
|
async done () {
|
||||||
|
let start_datetime, end_datetime
|
||||||
|
const [ start_hour, start_minute ] = this.time.start.split(':')
|
||||||
|
if (!this.time.end) {
|
||||||
|
this.time.end = this.time.start
|
||||||
|
}
|
||||||
|
const [ end_hour, end_minute ] = this.time.end.split(':')
|
||||||
|
if (this.event.multidate) {
|
||||||
|
start_datetime = moment(this.date.start)
|
||||||
|
.hour(start_hour).minute(start_minute)
|
||||||
|
end_datetime = moment(this.date.end)
|
||||||
|
.hour(end_hour).minute(end_minute)
|
||||||
|
} else {
|
||||||
|
start_datetime = moment(this.date)
|
||||||
|
.hour(start_hour).minute(start_minute)
|
||||||
|
end_datetime = moment(this.date)
|
||||||
|
.hour(end_hour).minute(end_minute)
|
||||||
|
}
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
if (this.event.image) {
|
||||||
|
formData.append('image', this.event.image, this.event.image.name)
|
||||||
|
}
|
||||||
|
formData.append('title', this.event.title)
|
||||||
|
formData.append('place_name', this.event.place.name)
|
||||||
|
formData.append('place_address', this.event.place.address)
|
||||||
|
formData.append('description', this.event.description)
|
||||||
|
formData.append('multidate', this.event.multidate)
|
||||||
|
formData.append('start_datetime', start_datetime)
|
||||||
|
formData.append('end_datetime', end_datetime)
|
||||||
|
if (this.edit) {
|
||||||
|
formData.append('id', this.event.id)
|
||||||
|
}
|
||||||
|
if (this.event.tags)
|
||||||
|
this.event.tags.forEach(tag => formData.append('tags[]', tag))
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.edit) {
|
||||||
|
await this.updateEvent(formData)
|
||||||
|
} else {
|
||||||
|
await this.addEvent(formData)
|
||||||
|
}
|
||||||
|
this.updateMeta()
|
||||||
|
this.$router.go(-1)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scope>
|
||||||
|
#tabss ul {
|
||||||
|
justify-content: space-evenly;
|
||||||
|
background: linear-gradient( #fff, #FFF 22px, #007bff, #fff 23px, #fff)
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabss ul .nav-link {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabss ul .nav-link.active {
|
||||||
|
background-color: #007bff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
11
client/src/filters.js
Normal file
11
client/src/filters.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import moment from 'moment'
|
||||||
|
moment.locale('it')
|
||||||
|
|
||||||
|
export default {
|
||||||
|
datetime (value) {
|
||||||
|
return moment(value).format('ddd, D MMMM HH:mm')
|
||||||
|
},
|
||||||
|
hour (value) {
|
||||||
|
return moment(value).format('HH:mm')
|
||||||
|
}
|
||||||
|
}
|
||||||
29
client/src/locale/en.js
Normal file
29
client/src/locale/en.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const en = {
|
||||||
|
where_explanation: 'Specify event\' place',
|
||||||
|
address_explanation: 'Insert address',
|
||||||
|
multidate_explanation: 'Multiple date?',
|
||||||
|
when_explanation: 'Select a day',
|
||||||
|
what_explanation: 'Event\'s title',
|
||||||
|
description_explanation: 'Describe the event',
|
||||||
|
date_explanation: 'Select the day',
|
||||||
|
dates_explanation: 'Select the days',
|
||||||
|
time_start_explanation: 'Insert start time',
|
||||||
|
time_end_explanation: 'You could insert end time',
|
||||||
|
media_explanation: 'You can upload a media',
|
||||||
|
tag_explanation: 'Insert a tag',
|
||||||
|
export_intro: `Sharing is caring.`,
|
||||||
|
export_feed_explanation: `Per seguire gli aggiornamenti da computer o smartphone senza la necessità di aprire periodicamente il sito, il metodo consigliato è quello dei Feed RSS.<br/>
|
||||||
|
Con i feed rss utilizzi un'apposita applicazione per ricevere aggiornamenti dai siti che più ti interessano.
|
||||||
|
È un buon metodo per seguire anche molti siti in modo molto rapido, senza necessità di creare un account o altre complicazioni.<br/>
|
||||||
|
|
||||||
|
Se hai Android, ti consigliamo <a href="https://play.google.com/store/apps/details?id=net.frju.flym">Flym</a> o Feeder<br />
|
||||||
|
Per iPhone/iPad puoi usare <a href="https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8">Feed4U</a><br />
|
||||||
|
Per il computer fisso/portatile consigliamo Feedbro, da installare all'interno <a href="https://addons.mozilla.org/en-GB/firefox/addon/feedbroreader/">di Firefox </a>o <a href="https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa">di Chrome</a> e compatibile con tutti i principali sistemi operativi.</p>
|
||||||
|
<br/>
|
||||||
|
Aggiungendo il link sopra, rimarrai aggiornata sui seguenti eventi:`,
|
||||||
|
SignIn: 'Sign In',
|
||||||
|
registration_email: `Hi, your registration will be confirmed soon.`,
|
||||||
|
register_explanation: `..`
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = en
|
||||||
63
client/src/locale/it.js
Normal file
63
client/src/locale/it.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
const it = {
|
||||||
|
'Add Event': 'Nuovo evento',
|
||||||
|
Where: 'Dove',
|
||||||
|
When: 'Quando',
|
||||||
|
What: 'Cosa',
|
||||||
|
Media: 'Locandina',
|
||||||
|
where_explanation: 'Specifica il luogo dell\'evento',
|
||||||
|
address_explanation: 'Inserisci l\'indirizzo',
|
||||||
|
multidate_explanation: 'Dura tanti giorni?',
|
||||||
|
when_explanation: 'Seleziona un giorno',
|
||||||
|
what_explanation: 'Titolo dell\'evento (es. Corteo Antimilitarista),',
|
||||||
|
description_explanation: 'Descrivi l\'evento, dajene di copia/incolla',
|
||||||
|
date_explanation: 'Seleziona il giorno',
|
||||||
|
dates_explanation: 'Seleziona i giorni',
|
||||||
|
time_start_explanation: 'Inserisci un orario di inizio',
|
||||||
|
time_end_explanation: 'Puoi inserire un orario di fine',
|
||||||
|
media_explanation: 'Se vuoi puoi mettere una locandina/manifesto',
|
||||||
|
tag_explanation: 'Puoi inserire un tag (es. concerto, corteo)',
|
||||||
|
export_intro: `Contrariamente alle piattaforme del capitalismo, che fanno di tutto per tenere
|
||||||
|
i dati e gli utenti al loro interno, crediamo che le informazioni, come le persone,
|
||||||
|
debbano essere libere.`,
|
||||||
|
export_feed_explanation: `Per seguire gli aggiornamenti da computer o smartphone senza la necessità di aprire periodicamente il sito, il metodo consigliato è quello dei Feed RSS.</p>
|
||||||
|
|
||||||
|
<p>Con i feed rss utilizzi un'apposita applicazione per ricevere aggiornamenti dai siti che più ti interessano. È un buon metodo per seguire anche molti siti in modo molto rapido, senza necessità di creare un account o altre complicazioni.</p>
|
||||||
|
|
||||||
|
<p>Se hai Android, ti consigliamo <a href="https://play.google.com/store/apps/details?id=net.frju.flym">Flym</a> o Feeder<br />
|
||||||
|
Per iPhone/iPad puoi usare <a href="https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8">Feed4U</a><br />
|
||||||
|
Per il computer fisso/portatile consigliamo Feedbro, da installare all'interno <a href="https://addons.mozilla.org/en-GB/firefox/addon/feedbroreader/">di Firefox </a>o <a href="https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa">di Chrome</a> e compatibile con tutti i principali sistemi operativi.</p>
|
||||||
|
|
||||||
|
Aggiungendo il link sopra, rimarrai aggiornata sui seguenti eventi:`,
|
||||||
|
Poster: 'Locandina',
|
||||||
|
Settings: 'Impostazioni',
|
||||||
|
Search: 'Cerca',
|
||||||
|
Send: 'Invia',
|
||||||
|
Register: 'Registrati',
|
||||||
|
Logout: 'Esci',
|
||||||
|
Login: 'Entra',
|
||||||
|
SignIn: 'Registrati',
|
||||||
|
Cancel: 'Annulla',
|
||||||
|
Next: 'Continua',
|
||||||
|
Prev: 'Indietro',
|
||||||
|
Username: 'Utente',
|
||||||
|
Description: 'Descrizione',
|
||||||
|
Deactivate: 'Disattiva',
|
||||||
|
Activate: 'Attiva',
|
||||||
|
'Remove Admin': 'Rimuovi Admin',
|
||||||
|
Users: 'Utenti',
|
||||||
|
Places: 'Luoghi',
|
||||||
|
Tags: 'Etichette',
|
||||||
|
Remove: 'Elimina',
|
||||||
|
Edit: 'Modifica',
|
||||||
|
Admin: 'Amministra',
|
||||||
|
Today: 'Oggi',
|
||||||
|
'Edit event': 'Modifica evento',
|
||||||
|
'New event': 'Nuovo evento',
|
||||||
|
registration_complete: 'Controlla la tua posta (anche la cartella spam)',
|
||||||
|
registration_email: `Ciao, la tua registrazione sarà confermata nei prossimi giorni. Riceverai una conferma non temere.`,
|
||||||
|
register_explanation: `I movimenti hanno bisogno di organizzarsi e autofinanziarsi. <br/>Questo è un dono per voi, non possiamo più vedervi usare le piattaforme del capitalismo. Solo eventi non commerciali e ovviamente antifascisti, antisessisti, antirazzisti.
|
||||||
|
<br/>Prima di poter pubblicare <strong>dobbiamo approvare l'account</strong>, considera che <strong>dietro questo sito ci sono delle persone</strong> di
|
||||||
|
carne e sangue, scrivici quindi due righe per farci capire che eventi vorresti pubblicare.`
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = it
|
||||||
68
client/src/main.js
Normal file
68
client/src/main.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import VueI18n from 'vue-i18n'
|
||||||
|
import BootstrapVue from 'bootstrap-vue'
|
||||||
|
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 ElementUI from 'element-ui'
|
||||||
|
import 'element-ui/lib/theme-chalk/index.css'
|
||||||
|
|
||||||
|
import itElementLocale from 'element-ui/lib/locale/lang/it'
|
||||||
|
import enElementLocale from 'element-ui/lib/locale/lang/en'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import store from './store'
|
||||||
|
|
||||||
|
import itLocale from '@/locale/it'
|
||||||
|
import enLocale from '@/locale/en'
|
||||||
|
|
||||||
|
// Use v-calendar, v-date-picker & v-popover components
|
||||||
|
Vue.use(VCalendar, {
|
||||||
|
firstDayOfWeek: 2
|
||||||
|
})
|
||||||
|
Vue.use(BootstrapVue)
|
||||||
|
Vue.use(VueI18n)
|
||||||
|
Vue.use(VueClipboard)
|
||||||
|
Vue.component('typeahead', Typeahead)
|
||||||
|
Vue.component('v-icon', Icon)
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
en: {
|
||||||
|
...enElementLocale,
|
||||||
|
...enLocale
|
||||||
|
},
|
||||||
|
it: {
|
||||||
|
...itElementLocale,
|
||||||
|
...itLocale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create VueI18n instance with options
|
||||||
|
const i18n = new VueI18n({
|
||||||
|
locale: 'it', // set locale
|
||||||
|
messages // set locale messages
|
||||||
|
})
|
||||||
|
|
||||||
|
Vue.use(ElementUI, { i18n: (key, value) => i18n.t(key, value) })
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
Vue.config.lang = 'it'
|
||||||
|
Vue.config.devtools = true
|
||||||
|
Vue.config.silent = false
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
i18n,
|
||||||
|
router,
|
||||||
|
store,
|
||||||
|
render: h => h(App)
|
||||||
|
}).$mount('#app')
|
||||||
51
client/src/router.js
Normal file
51
client/src/router.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import Router from 'vue-router'
|
||||||
|
|
||||||
|
import Settings from './components/Settings'
|
||||||
|
import newEvent from './components/newEvent'
|
||||||
|
import EventDetail from './components/EventDetail'
|
||||||
|
import Login from './components/Login'
|
||||||
|
import Register from './components/Register'
|
||||||
|
import Export from './components/Export'
|
||||||
|
import Admin from './components/Admin'
|
||||||
|
|
||||||
|
Vue.use(Router)
|
||||||
|
|
||||||
|
export default new Router({
|
||||||
|
mode: 'history',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/admin',
|
||||||
|
components: { modal: Admin }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/register',
|
||||||
|
components: { modal: Register }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
components: { modal: Login }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/new_event',
|
||||||
|
components: { modal: newEvent }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
components: { modal: Settings }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/event/:id',
|
||||||
|
components: { modal: EventDetail }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/edit/:id',
|
||||||
|
components: { modal: newEvent },
|
||||||
|
props: { edit: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/export/:type',
|
||||||
|
components: { modal: Export }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
119
client/src/store.js
Normal file
119
client/src/store.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import VuexPersistence from 'vuex-persist'
|
||||||
|
import api from './api'
|
||||||
|
Vue.use(Vuex)
|
||||||
|
|
||||||
|
const vuexLocal = new VuexPersistence({
|
||||||
|
storage: window.localStorage,
|
||||||
|
reducer: state => ({ logged: state.logged, user: state.user, token: state.token })
|
||||||
|
})
|
||||||
|
|
||||||
|
export default new Vuex.Store({
|
||||||
|
plugins: [vuexLocal.plugin],
|
||||||
|
getters: {
|
||||||
|
token: state => state.token
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
logged: false,
|
||||||
|
user: {},
|
||||||
|
token: '',
|
||||||
|
events: [],
|
||||||
|
tags: [],
|
||||||
|
places: [],
|
||||||
|
//
|
||||||
|
filters: {
|
||||||
|
tags: [],
|
||||||
|
places: [],
|
||||||
|
hidePast: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
logout (state) {
|
||||||
|
state.logged = false
|
||||||
|
state.token = ''
|
||||||
|
state.user = {}
|
||||||
|
},
|
||||||
|
login (state, user) {
|
||||||
|
state.logged = true
|
||||||
|
state.user = user.user
|
||||||
|
state.token = user.token
|
||||||
|
},
|
||||||
|
setEvents (state, events) {
|
||||||
|
state.events = events
|
||||||
|
},
|
||||||
|
addEvent (state, event) {
|
||||||
|
state.events.push(event)
|
||||||
|
},
|
||||||
|
updateEvent (state, event) {
|
||||||
|
state.events = state.events.map(e => {
|
||||||
|
if (e.id !== event.id) return e
|
||||||
|
return event
|
||||||
|
})
|
||||||
|
},
|
||||||
|
delEvent (state, eventId) {
|
||||||
|
state.events = state.events.filter(ev => ev.id !== eventId)
|
||||||
|
},
|
||||||
|
update (state, { tags, places }) {
|
||||||
|
state.tags = tags
|
||||||
|
state.places = places
|
||||||
|
},
|
||||||
|
// search
|
||||||
|
addSearchTag (state, tag) {
|
||||||
|
if (!state.filters.tags.find(t => t === tag.tag)) {
|
||||||
|
state.filters.tags.push(tag.tag)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setSearchTags (state, tags) {
|
||||||
|
state.filters.tags = tags
|
||||||
|
},
|
||||||
|
addSearchPlace (state, place) {
|
||||||
|
if (state.filters.places.find(p => p.name === place.name)) {
|
||||||
|
state.filters.places.push(place)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setSearchPlaces (state, places) {
|
||||||
|
state.filters.places = places
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async updateEvents ({ commit }, date) {
|
||||||
|
const events = await api.getAllEvents(date.month - 1, date.year)
|
||||||
|
commit('setEvents', events)
|
||||||
|
},
|
||||||
|
async updateMeta ({ commit }) {
|
||||||
|
const { tags, places } = await api.getMeta()
|
||||||
|
commit('update', { tags, places })
|
||||||
|
},
|
||||||
|
async addEvent ({ commit }, formData) {
|
||||||
|
const event = await api.addEvent(formData)
|
||||||
|
commit('addEvent', event)
|
||||||
|
},
|
||||||
|
async updateEvent ({ commit }, formData) {
|
||||||
|
const event = await api.updateEvent(formData)
|
||||||
|
commit('updateEvent', event)
|
||||||
|
},
|
||||||
|
delEvent ({ commit }, eventId) {
|
||||||
|
commit('delEvent', eventId)
|
||||||
|
},
|
||||||
|
login ({ commit }, user) {
|
||||||
|
commit('login', user)
|
||||||
|
},
|
||||||
|
logout ({ commit }) {
|
||||||
|
commit('logout')
|
||||||
|
},
|
||||||
|
// search
|
||||||
|
addSearchTag ({ commit }, tag) {
|
||||||
|
commit('addSearchTag', tag)
|
||||||
|
},
|
||||||
|
setSearchTags ({ commit }, tags) {
|
||||||
|
commit('setSearchTags', tags)
|
||||||
|
},
|
||||||
|
addSearchPlace ({ commit }, place) {
|
||||||
|
commit('addSearchPlace', place)
|
||||||
|
},
|
||||||
|
setSearchPlaces ({ commit }, places) {
|
||||||
|
commit('setSearchPlaces', places)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
9
client/vue.config.js
Normal file
9
client/vue.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
module.exports = {
|
||||||
|
devServer: {
|
||||||
|
disableHostCheck: true
|
||||||
|
},
|
||||||
|
transpileDependencies: [
|
||||||
|
/\bvue-awesome\b/,
|
||||||
|
'vuex-persist'
|
||||||
|
]
|
||||||
|
}
|
||||||
10875
client/yarn.lock
Normal file
10875
client/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,35 +0,0 @@
|
|||||||
const path = require('path')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
// environment
|
|
||||||
env: 'development',
|
|
||||||
locale: 'it',
|
|
||||||
|
|
||||||
title: 'Gancio',
|
|
||||||
description: 'Un calendario dei movimenti piemontesi',
|
|
||||||
|
|
||||||
// base url
|
|
||||||
baseurl: 'http://localhost:8080',
|
|
||||||
apiurl: 'http://localhost:9000/api',
|
|
||||||
|
|
||||||
|
|
||||||
// db configuration
|
|
||||||
db: {
|
|
||||||
'storage': path.join(__dirname, '/../db.sqlite'),
|
|
||||||
'dialect': 'sqlite'
|
|
||||||
},
|
|
||||||
admin: 'lesion@autistici.org',
|
|
||||||
|
|
||||||
// email configuration
|
|
||||||
smtp: {
|
|
||||||
host: 'mail.example.com',
|
|
||||||
secure: true,
|
|
||||||
auth: {
|
|
||||||
user: 'user@example.com',
|
|
||||||
pass: 'password'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// jwt secret
|
|
||||||
secret: 'nonosecretsuper'
|
|
||||||
}
|
|
||||||
27
config/config.development.json
Normal file
27
config/config.development.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"env": "development",
|
||||||
|
"locale": "it",
|
||||||
|
|
||||||
|
"title": "Gancio",
|
||||||
|
"description": "Un calendario dei movimenti piemontesi",
|
||||||
|
|
||||||
|
"baseurl": "http://localhost:8080",
|
||||||
|
"apiurl": "http://localhost:9000/api",
|
||||||
|
|
||||||
|
"db": {
|
||||||
|
"storage": "./db.sqlite",
|
||||||
|
"dialect": "sqlite"
|
||||||
|
},
|
||||||
|
"admin": "lesion@autistici.org",
|
||||||
|
|
||||||
|
"smtp": {
|
||||||
|
"host": "mail.example.org",
|
||||||
|
"secure": true,
|
||||||
|
"auth": {
|
||||||
|
"user": "events@example.org",
|
||||||
|
"pass": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"secret": "nonosecretsuper"
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
// environment
|
|
||||||
env: 'production',
|
|
||||||
locale: 'en',
|
|
||||||
|
|
||||||
title: 'Put here your site name',
|
|
||||||
description: 'A calendar for radical communities',
|
|
||||||
|
|
||||||
// base url
|
|
||||||
baseurl: 'https://example.com',
|
|
||||||
apiurl: 'https://example.com/api',
|
|
||||||
|
|
||||||
// db configuration
|
|
||||||
db: {
|
|
||||||
},
|
|
||||||
admin: 'admin@example.com',
|
|
||||||
|
|
||||||
// email configuration
|
|
||||||
smtp: {
|
|
||||||
host: 'mail.example.com',
|
|
||||||
secure: true,
|
|
||||||
auth: {
|
|
||||||
user: 'admin@example.com',
|
|
||||||
pass: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// jwt secret
|
|
||||||
secret: 'randomstringhere'
|
|
||||||
}
|
|
||||||
25
config/config.production.json
Normal file
25
config/config.production.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"env": "production",
|
||||||
|
"locale": "en",
|
||||||
|
|
||||||
|
"title": "Put here your site name",
|
||||||
|
"description": "A calendar for radical communities",
|
||||||
|
|
||||||
|
"baseurl": "https://example.com",
|
||||||
|
"apiurl": "https://example.com/api",
|
||||||
|
|
||||||
|
"db": {
|
||||||
|
},
|
||||||
|
"admin": "admin@example.com",
|
||||||
|
|
||||||
|
"smtp": {
|
||||||
|
"host": "mail.example.com",
|
||||||
|
"secure":"true",
|
||||||
|
"auth": {
|
||||||
|
"user": "admin@example.com",
|
||||||
|
"pass": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"secret": "randomstringhere"
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ const bodyParser = require('body-parser')
|
|||||||
const api = require('./app/api')
|
const api = require('./app/api')
|
||||||
const cors = require('cors')
|
const cors = require('cors')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const db = require('./app/db')
|
|
||||||
const port = process.env.PORT || 8080
|
const port = process.env.PORT || 8080
|
||||||
|
|
||||||
app.use(bodyParser.urlencoded({ extended: false }))
|
app.use(bodyParser.urlencoded({ extended: false }))
|
||||||
|
|||||||
Reference in New Issue
Block a user