cleaning master

This commit is contained in:
lesion
2019-06-08 13:28:10 +02:00
parent 07d3601fd6
commit c2b69d15a7
81 changed files with 0 additions and 15231 deletions

View File

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

View File

@@ -1,5 +0,0 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

21
.env
View File

@@ -1,21 +0,0 @@
NODE_ENV=production
# complete url (use https here!)
BASE_URL=http://localhost:12300
# node server will listen here
PORT=12300
# your instance's name and description
TITLE=Gancio
DESCRIPTION="description"
# where emails comes from
ADMIN_EMAIL=admin@example.org
SMTP_HOST=mail.example.org
SMTP_USER=admin@example.org
SMTP_PASS=password
# please put a random string here
SECRET=secret

View File

@@ -1,6 +0,0 @@
module.exports = {
"extends": "standard",
"rules": {
"camelcase": 0
}
};

25
.gitignore vendored
View File

@@ -1,25 +0,0 @@
.DS_Store
node_modules
client/dist
uploads
# local env files
.env.production
db.sqlite
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

View File

@@ -1,33 +0,0 @@
FROM node:10
WORKDIR /usr/src/app
COPY package.json .
COPY pm2.json .
COPY .env.production .
# install backend dependencies
RUN yarn
# copy source
COPY app app/
COPY client client/
# install nodemon
RUN yarn global add pm2
WORKDIR /usr/src/app/client
# install frontend dependencies
RUN yarn
# build frontend
RUN export $(cat /usr/src/app/.env.production); yarn build
WORKDIR /usr/src/app
EXPOSE 12300
CMD [ "pm2-runtime", "start", "pm2.json" ]

View File

@@ -1,41 +0,0 @@
# gancio
an event manager for radical communities
:warning: Gancio is under heavy development,
if something is not working as expected, it's expected :D
#### Install
We provide a docker way to run **gancio** but you can also manually install it.
```
git clone https://git.lattuga.net/lesion/gancio.git
cd gancio
# copy .env into .env.production and edit it
docker-compose up -d
```
#### Development
both backend (`/app`) and frontend(`/client`) are in this repo.
backend stack: node.js, express, sequelize.
frontend stack: vue, webpack, boostrap
```
git clone https://git.lattuga.net/lesion/gancio.git
cd gancio
# install back-end dependencies
yarn
# run back-end in development mode
yarn dev
cd client
# install front-end dependencies
yarn
# run front-end in development mode
yarn dev
```
#### Migrate / Backup
- db
- images

View File

@@ -1,79 +0,0 @@
const express = require('express')
const { fillUser, isAuth, isAdmin } = require('./auth')
const eventController = require('./controller/event')
const exportController = require('./controller/export')
const userController = require('./controller/user')
const settingsController = require('./controller/settings')
// const botController = require('./controller/bot')
const multer = require('multer')
const storage = require('./storage')({
destination: 'uploads/'
})
const upload = multer({ storage })
const api = express.Router()
// login
api.post('/login', userController.login)
api.post('/user/recover', userController.forgotPassword)
api.post('/user/check_recover_code', userController.checkRecoverCode)
api.post('/user/recover_password', userController.updatePasswordWithRecoverCode)
api.route('/user')
// register
.post(userController.register)
// get current user
.get(isAuth, userController.current)
// update user (eg. confirm)
.put(isAuth, isAdmin, userController.update)
// get all users
api.get('/users', isAuth, isAdmin, userController.getAll)
// update a tag (modify color)
api.put('/tag', isAuth, isAdmin, eventController.updateTag)
// update a place (modify address..)
api.put('/place', isAuth, isAdmin, eventController.updatePlace)
api.route('/user/event')
// add event
.post(fillUser, upload.single('image'), userController.addEvent)
// update event
.put(isAuth, upload.single('image'), userController.updateEvent)
// remove event
api.delete('/user/event/:id', isAuth, userController.delEvent)
// get tags/places
api.get('/event/meta', eventController.getMeta)
// get unconfirmed events
api.get('/event/unconfirmed', isAuth, isAdmin, eventController.getUnconfirmed)
// add event notification
api.post('/event/notification', eventController.addNotification)
api.delete('/event/notification/:code', eventController.delNotification)
api.get('/settings', settingsController.getAdminSettings)
api.post('/settings', settingsController.setAdminSetting)
// get event
api.get('/event/:event_id', eventController.get)
// confirm event
api.get('/event/confirm/:event_id', isAuth, isAdmin, eventController.confirm)
api.get('/event/unconfirm/:event_id', isAuth, isAdmin, eventController.unconfirm)
// export events (rss/ics)
api.get('/export/:type', exportController.export)
// get events in this range
api.get('/event/:year/:month', eventController.getAll)
// mastodon oauth auth
api.post('/user/getauthurl', isAuth, userController.getAuthURL)
api.post('/user/code', isAuth, userController.code)
module.exports = api

View File

@@ -1,32 +0,0 @@
const jwt = require('jsonwebtoken')
const config = require('./config')
const User = require('./models/user')
const { Op } = require('sequelize')
const Auth = {
async fillUser (req, res, next) {
const token = req.body.token || req.params.token || req.headers['x-access-token']
if (!token) return next()
jwt.verify(token, config.secret, async (err, decoded) => {
if (err) return next()
req.user = await User.findOne({ where: { email: { [Op.eq]: decoded.email }, is_active: true } })
next()
})
},
async isAuth (req, res, next) {
const token = req.body.token || req.params.token || req.headers['x-access-token']
if (!token) return res.status(403).send({ message: 'Token not found' })
jwt.verify(token, config.secret, async (err, decoded) => {
if (err) return res.status(403).send({ message: 'Failed to authenticate token ' + err })
req.user = await User.findOne({ where: { email: { [Op.eq]: decoded.email }, is_active: true } })
if (!req.user) return res.status(403).send({ message: 'Failed to authenticate token ' + err })
next()
})
},
async isAdmin (req, res, next) {
if (req.user.is_admin && req.user.is_active) return next()
return res.status(403).send({ message: 'Admin needed' })
}
}
module.exports = Auth

View File

@@ -1,24 +0,0 @@
/* backend configuration */
const env = process.env.NODE_ENV || 'development'
const db = require('./config/config.json')[env]
module.exports = {
locale: process.env.LOCALE || 'it',
title: process.env.TITLE || 'GANCIO',
description: process.env.DESCRIPTION || 'A calendar for radical communities',
baseurl: process.env.BASE_URL || 'http://localhost:8080',
apiurl: env === 'production' ? process.env.BASE_URL + '/api' : 'http://localhost:9000',
db,
admin: process.env.ADMIN_EMAIL,
smtp: {
host: process.env.SMTP_HOST,
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
},
secret: process.env.SECRET || 'notsosecret'
}

View File

@@ -1,14 +0,0 @@
{
"development": {
"storage": "/home/les/dev/hacklab/eventi/db.sqlite",
"dialect": "sqlite",
"logging": false
},
"production": {
"username": "docker",
"password": "docker",
"database": "gancio",
"host": "db",
"dialect": "postgres"
}
}

View File

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

View File

@@ -1,162 +0,0 @@
const { User, Event, Comment, Tag, Place, Notification } = require('../model')
const moment = require('moment')
const { Op } = require('sequelize')
const lodash = require('lodash')
const crypto = require('crypto')
const eventController = {
async addComment (req, res) {
// comment could be added to an event or to another comment
let event = await Event.findOne({ where: { activitypub_id: { [Op.eq]: req.body.id } } })
if (!event) {
const comment = await Comment.findOne({ where: { activitypub_id: { [Op.eq]: req.body.id } }, include: Event })
event = comment.event
}
const comment = new Comment(req.body)
event.addComment(comment)
res.json(comment)
},
async getMeta (req, res) {
const places = await Place.findAll()
const tags = await Tag.findAll()
res.json({ tags, places })
},
async getNotifications (event) {
function match (event, filters) {
// matches if no filter specified
if (!filters) return true
// check for visibility
if (typeof filters.is_visible !== 'undefined' && filters.is_visible !== event.is_visible) return false
if (!filters.tags && !filters.places) return true
if (!filters.tags.length && !filters.places.length) return true
if (filters.tags.length) {
const m = lodash.intersection(event.tags.map(t => t.tag), filters.tags)
if (m.length > 0) return true
}
if (filters.places.length) {
if (filters.places.find(p => p === event.place.name)) {
return true
}
}
}
const notifications = await Notification.findAll()
// get notification that matches with selected event
return notifications.filter(notification => match(event, notification.filters))
},
async updateTag (req, res) {
const tag = await Tag.findByPk(req.body.tag)
if (tag) {
res.json(await tag.update(req.body))
} else {
res.sendStatus(404)
}
},
async updatePlace (req, res) {
const place = await Place.findByPk(req.body.id)
await place.update(req.body)
res.json(place)
},
async get (req, res) {
const id = req.params.event_id
const event = await Event.findByPk(id, { include: [User, Tag, Comment, Place] })
res.json(event)
},
async confirm (req, res) {
const id = req.params.event_id
const event = await Event.findByPk(id)
try {
await event.update({ is_visible: true })
// insert notification
const notifications = await eventController.getNotifications(event)
await event.setNotifications(notifications)
res.sendStatus(200)
} catch (e) {
res.sendStatus(404)
}
},
async unconfirm (req, res) {
const id = req.params.event_id
const event = await Event.findByPk(id)
try {
await event.update({ is_visible: false })
res.sendStatus(200)
} catch (e) {
res.sendStatus(404)
}
},
async getUnconfirmed (req, res) {
const events = await Event.findAll({
where: {
is_visible: false
},
order: [['start_datetime', 'ASC']],
include: [Tag, Place]
})
res.json(events)
},
async addNotification (req, res) {
try {
const notification = {
filters: { is_visible: true },
email: req.body.email,
type: 'mail',
remove_code: crypto.randomBytes(16).toString('hex')
}
await Notification.create(notification)
res.sendStatus(200)
} catch (e) {
res.sendStatus(404)
}
},
async delNotification (req, res) {
const remove_code = req.params.code
try {
const notification = await Notification.findOne({ where: { remove_code: { [Op.eq]: remove_code } } })
await notification.destroy()
} catch (e) {
return res.sendStatus(404)
}
res.sendStatus(200)
},
async getAll (req, res) {
// this is due how v-calendar shows dates
let start = moment().year(req.params.year).month(req.params.month)
.startOf('month').startOf('isoWeek')
let end = moment().year(req.params.year).month(req.params.month).endOf('month')
const shownDays = end.diff(start, 'days')
if (shownDays <= 34) end = end.add(1, 'week')
end = end.endOf('isoWeek')
const events = await Event.findAll({
where: {
is_visible: true,
[Op.and]: [
{ start_datetime: { [Op.gte]: start } },
{ start_datetime: { [Op.lte]: end } }
]
},
order: [['start_datetime', 'ASC']],
include: [User, Comment, Tag, Place]
})
res.json(events)
}
}
module.exports = eventController

View File

@@ -1,64 +0,0 @@
const { Event, Comment, Tag, Place } = require('../model')
const { Op } = require('sequelize')
const config = require('../config')
const moment = require('moment')
const ics = require('ics')
const exportController = {
async export (req, res) {
console.log('type ', req.params.type)
const type = req.params.type
const tags = req.query.tags
const places = req.query.places
const whereTag = {}
const wherePlace = {}
const yesterday = moment().subtract('1', 'day')
if (tags) {
whereTag.tag = tags.split(',')
}
if (places) {
wherePlace.name = places.split(',')
}
const events = await Event.findAll({
where: { is_visible: true, start_datetime: { [Op.gte]: yesterday } },
include: [Comment, {
model: Tag,
where: whereTag
}, { model: Place, where: wherePlace } ]
})
switch (type) {
case 'feed':
return exportController.feed(res, events.slice(0, 20))
case 'ics':
return exportController.ics(res, events)
}
},
async feed (res, events) {
res.type('application/rss+xml; charset=UTF-8')
res.render('feed/rss.pug', { events, config, moment })
},
async ics (res, events) {
const eventsMap = events.map(e => {
const tmpStart = moment(e.start_datetime)
const tmpEnd = moment(e.end_datetime)
const start = [tmpStart.year(), tmpStart.month() + 1, tmpStart.date(), tmpStart.hour(), tmpStart.minute()]
const end = [tmpEnd.year(), tmpEnd.month() + 1, tmpEnd.date(), tmpEnd.hour(), tmpEnd.minute()]
return {
start,
end,
title: e.title,
description: e.description,
location: e.place.name + ' ' + e.place.address
}
})
res.type('text/calendar; charset=UTF-8')
const { error, value } = ics.createEvents(eventsMap)
console.log(error, value)
res.send(value)
}
}
module.exports = exportController

View File

@@ -1,27 +0,0 @@
const { Settings } = require('../model')
const settingsController = {
async setAdminSetting (key, value) {
await Settings.findOrCreate({ where: { key },
defaults: { value } })
.spread((settings, created) => {
if (!created) return settings.update({ value })
})
},
async getAdminSettings (req, res) {
const settings = await settingsController.settings()
res.json(settings)
},
async settings () {
const settings = await Settings.findAll()
const map = {}
settings.forEach(setting => {
map[setting.key] = setting.value
})
return map
}
}
module.exports = settingsController

View File

@@ -1,283 +0,0 @@
const jwt = require('jsonwebtoken')
const Mastodon = require('mastodon-api')
const User = require('../models/user')
const { Event, Tag, Place } = require('../models/event')
const settingsController = require('./settings')
const eventController = require('./event')
const config = require('../config')
const mail = require('../mail')
const { Op } = require('sequelize')
const fs = require('fs')
const path = require('path')
const crypto = require('crypto')
const userController = {
async login (req, res) {
// find the user
const user = await User.findOne({ where: { email: { [Op.eq]: req.body.email } } })
if (!user) {
res.status(404).json({ success: false, message: 'AUTH_FAIL' })
} else if (user) {
if (!user.is_active) {
res.status(403).json({ success: false, message: 'NOT_CONFIRMED' })
// check if password matches
} else if (!await user.comparePassword(req.body.password)) {
res.status(403).json({ success: false, message: 'AUTH_FAIL' })
} else {
// if user is found and password is right
// create a token
const payload = { email: user.email }
var token = jwt.sign(payload, config.secret)
res.json({
success: true,
message: 'Enjoy your token!',
token,
user
})
}
}
},
async setToken (req, res) {
req.user.mastodon_auth = req.body
await req.user.save()
res.json(req.user)
},
async delEvent (req, res) {
const event = await Event.findByPk(req.params.id)
// check if event is mine (or user is admin)
if (event && (req.user.is_admin || req.user.id === event.userId)) {
if (event.image_path) {
const old_path = path.resolve(__dirname, '..', '..', 'uploads', event.image_path)
const old_thumb_path = path.resolve(__dirname, '..', '..', 'uploads', 'thumb', event.image_path)
await fs.unlink(old_path)
await fs.unlink(old_thumb_path)
}
await event.destroy()
res.sendStatus(200)
} else {
res.sendStatus(403)
}
},
// ADD EVENT
async addEvent (req, res) {
const body = req.body
// remove description tag and create anchor tags
const description = body.description
.replace(/(<([^>]+)>)/ig, '')
.replace(/(https?:\/\/[^\s]+)/g, '<a href="$1">$1</a>')
const eventDetails = {
title: body.title,
description,
multidate: body.multidate,
start_datetime: body.start_datetime,
end_datetime: body.end_datetime,
is_visible: !!req.user
}
if (req.file) {
eventDetails.image_path = req.file.filename
}
let event = await Event.create(eventDetails)
// create place
let place
try {
place = await Place.findOrCreate({ where: { name: body.place_name },
defaults: { address: body.place_address } })
.spread((place, created) => place)
await event.setPlace(place)
} catch (e) {
console.error(e)
}
// create/assign tags
if (body.tags) {
await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true })
const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } })
await event.addTags(tags)
}
if (req.user) await req.user.addEvent(event)
event = await Event.findByPk(event.id, { include: [User, Tag, Place] })
// insert notifications
const notifications = await eventController.getNotifications(event)
await event.setNotifications(notifications)
return res.json(event)
},
async updateEvent (req, res) {
const body = req.body
const event = await Event.findByPk(body.id)
if (!req.user.is_admin && event.userId !== req.user.id) {
return res.sendStatus(403)
}
if (req.file) {
if (event.image_path) {
const old_path = path.resolve(__dirname, '..', '..', 'uploads', event.image_path)
const old_thumb_path = path.resolve(__dirname, '..', '..', 'uploads', 'thumb', event.image_path)
await fs.unlink(old_path, e => console.error(e))
await fs.unlink(old_thumb_path, e => console.error(e))
}
body.image_path = req.file.filename
}
body.description = body.description
.replace(/(<([^>]+)>)/ig, '') // remove all tags from description
.replace(/(https?:\/\/[^\s]+)/g, '<a href="$1">$1</a>') // add links
await event.update(body)
let place
try {
place = await Place.findOrCreate({ where: { name: body.place_name },
defaults: { address: body.place_address } })
.spread((place, created) => place)
} catch (e) {
console.log('error', e)
}
await event.setPlace(place)
await event.setTags([])
if (body.tags) {
await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true })
const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } })
await event.addTags(tags)
}
const newEvent = await Event.findByPk(event.id, { include: [User, Tag, Place] })
return res.json(newEvent)
},
async getAuthURL (req, res) {
const instance = req.body.instance
const is_admin = req.body.admin && req.user.is_admin
const callback = `${config.baseurl}/${is_admin ? 'admin/oauth' : 'settings'}`
const { client_id, client_secret } = await Mastodon.createOAuthApp(`https://${instance}/api/v1/apps`,
config.title, 'read write', callback)
const url = await Mastodon.getAuthorizationUrl(client_id, client_secret,
`https://${instance}`, 'read write', callback)
if (is_admin) {
await settingsController.setAdminSetting('mastodon_auth', { client_id, client_secret, instance })
} else {
req.user.mastodon_auth = { client_id, client_secret, instance }
await req.user.save()
}
res.json(url)
},
async code (req, res) {
const { code, is_admin } = req.body
let client_id, client_secret, instance
const callback = `${config.baseurl}/${is_admin ? 'admin/oauth' : 'settings'}`
if (is_admin) {
const settings = await settingsController.settings();
({ client_id, client_secret, instance } = settings.mastodon_auth)
} else {
({ client_id, client_secret, instance } = req.user.mastodon_auth)
}
try {
const token = await Mastodon.getAccessToken(client_id, client_secret, code,
`https://${instance}`, callback)
const mastodon_auth = { client_id, client_secret, access_token: token, instance }
if (is_admin) {
await settingsController.setAdminSetting('mastodon_auth', mastodon_auth)
res.json(instance)
} else {
req.user.mastodon_auth = mastodon_auth
await req.user.save()
// await bot.add(req.user, token)
res.json(req.user)
}
} catch (e) {
res.json(e)
}
},
async forgotPassword (req, res) {
const email = req.body.email
const user = await User.findOne({ where: { email: { [Op.eq]: email } } })
if (!user) return res.sendStatus(200)
user.recover_code = crypto.randomBytes(16).toString('hex')
mail.send(user.email, 'recover', { user, config })
await user.save()
res.sendStatus(200)
},
async checkRecoverCode (req, res) {
const recover_code = req.body.recover_code
if (!recover_code) return res.sendStatus(400)
const user = await User.findOne({ where: { recover_code: { [Op.eq]: recover_code } } })
if (!user) return res.sendStatus(400)
res.json(user)
},
async updatePasswordWithRecoverCode (req, res) {
const recover_code = req.body.recover_code
if (!recover_code) return res.sendStatus(400)
const password = req.body.password
const user = await User.findOne({ where: { recover_code: { [Op.eq]: recover_code } } })
if (!user) return res.sendStatus(400)
user.password = password
await user.save()
res.sendStatus(200)
},
async current (req, res) {
res.json(req.user)
},
async getAll (req, res) {
const users = await User.findAll({
order: [['createdAt', 'DESC']]
})
res.json(users)
},
async update (req, res) {
const user = await User.findByPk(req.body.id)
if (user) {
if (!user.is_active && req.body.is_active) {
await mail.send(user.email, 'confirm', { user, config })
}
await user.update(req.body)
res.json(user)
} else {
res.sendStatus(400)
}
},
async register (req, res) {
const n_users = await User.count()
try {
if (n_users === 0) {
// the first registered user will be an active admin
req.body.is_active = req.body.is_admin = true
} else {
req.body.is_active = false
}
const user = await User.create(req.body)
try {
mail.send([user.email, config.admin], 'register', { user, config })
} catch (e) {
return res.status(400).json(e)
}
const payload = { email: user.email }
const token = jwt.sign(payload, config.secret)
res.json({ user, token })
} catch (e) {
res.status(404).json(e)
}
}
}
module.exports = userController

View File

@@ -1,61 +0,0 @@
const mail = require('./mail')
const bot = require('./controller/bot')
const settingsController = require('./controller/settings')
const config = require('./config.js')
const { Event, Notification, EventNotification,
User, Place, Tag } = require('./model')
let settings
async function sendNotification (notification, event, eventNotification) {
const promises = []
switch (notification.type) {
case 'mail':
return mail.send(notification.email, 'event', { event, config, notification })
case 'admin_email':
const admins = await User.findAll({ where: { is_admin: true } })
const admin_emails = admins.map(admin => admin.email)
return mail.send(admin_emails, 'event', { event, to_confirm: true, notification })
case 'mastodon':
// instance publish
if (settings.mastodon_auth.instance && settings.mastodon_auth.access_token) {
const b = bot.post(settings.mastodon_auth, event)
promises.push(b)
}
// user publish
if (event.user && event.user.mastodon_auth && event.user.mastodon_auth.access_token) {
const b = bot.post(event.user.mastodon_auth, event).then(ret => {
event.activitypub_id = ret.id
return event.save()
})
promises.push(b)
}
break
}
return Promise.all(promises)
}
async function loop () {
settings = await settingsController.settings()
// get all event notification in queue
const eventNotifications = await EventNotification.findAll({ where: { status: 'new' } })
const promises = eventNotifications.map(async e => {
const event = await Event.findByPk(e.eventId, { include: [User, Place, Tag] })
if (!event.place) return
const notification = await Notification.findByPk(e.notificationId)
try {
await sendNotification(notification, event, e)
e.status = 'sent'
return e.save()
} catch (err) {
console.error(err)
e.status = 'error'
return e.save()
}
})
return Promise.all(promises)
}
setInterval(loop, 260000)
loop()

View File

@@ -1,4 +0,0 @@
const Sequelize = require('sequelize')
const conf = require('./config.js')
const db = new Sequelize(conf.db)
module.exports = db

View File

@@ -1,4 +0,0 @@
p= t('confirm_email')
hr
small #{config.baseurl}

View File

@@ -1,18 +0,0 @@
h3 #{event.title}
p Dove: #{event.place.name} - #{event.place.address}
p Quando: #{datetime(event.start_datetime)}
br
if event.image_path
<img style="width: 100%" src="#{config.apiurl}/uploads/#{event.image_path}" />
p #{event.description}
each tag in event.tags
span ##{tag.tag}
br
<a href="#{config.baseurl}/event/#{event.id}">#{config.baseurl}/event/#{event.id}</a>
hr
if to_confirm
p Puoi confermare questo evento <a href="#{config.baseurl}/admin/confirm/#{event.id}">qui</a>
else
p Puoi eliminare queste notifiche <a href="#{config.baseurl}/del_notification/#{notification.remove_code}">qui</a>
<a href="#{config.baseurl}">#{config.title} - #{config.description}</a>

View File

@@ -1 +0,0 @@
= `[${config.title}] ${event.title} @${event.place.name} ${datetime(event.start_datetime)}`

View File

@@ -1,8 +0,0 @@
table {
width: 100%;
border-collapse: collapse;
}
table, th, td {
border: 1px solid #555;
}

View File

@@ -1,3 +0,0 @@
p= t('recover_email')
<a href="#{config.baseurl}/recover/#{user.recover_code}">#{t('press here')}</a>

View File

@@ -1 +0,0 @@
= `[Gancio] Richiesta password recovery`

View File

@@ -1,6 +0,0 @@
p= t('registration_email')
hr
small #{config.title} / #{config.description}
br
small #{config.baseurl}

View File

@@ -1 +0,0 @@
= `[Gancio] Richiesta registrazione`

View File

@@ -1,44 +0,0 @@
const Email = require('email-templates')
const path = require('path')
const config = require('./config')
const moment = require('moment')
moment.locale('it')
const mail = {
send (addresses, template, locals) {
const email = new Email({
views: { root: path.join(__dirname, 'emails') },
juice: true,
juiceResources: {
preserveImportant: true,
webResources: {
relativeTo: path.join(__dirname, 'emails')
}
},
message: {
from: `${config.title} <${config.smtp.auth.user}>`
},
send: true,
i18n: {
locales: ['en', 'es', 'it'],
defaultLocale: config.locale
},
transport: config.smtp
})
return email.send({
template,
message: {
to: addresses,
bcc: config.admin
},
locals: {
...locals,
locale: config.locale,
config,
datetime: datetime => moment(datetime).format('ddd, D MMMM HH:mm')
}
})
}
}
module.exports = mail

View File

@@ -1,10 +0,0 @@
module.exports = {
up: (queryInterface, Sequelize) => {
// return queryInterface.addColumn('EventNotifications', 'status',
// { type: Sequelize.ENUM, values: ['new', 'sent', 'error'], index: true, defaultValue: 'new' })
},
down: (queryInterface, Sequelize) => {
return queryInterface.removeColumn('EventNotifications', 'status')
}
}

View File

@@ -1,10 +0,0 @@
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.addColumn('users', 'recover_code',
{ type: Sequelize.STRING })
},
down: (queryInterface, Sequelize) => {
return queryInterface.removeColumn('users', 'recover_code')
}
}

View File

@@ -1,14 +0,0 @@
const User = require('./models/user')
const { Event, Comment, Tag, Place, Notification, EventNotification } = require('./models/event')
const Settings = require('./models/settings')
module.exports = {
User,
Event,
Comment,
Tag,
Place,
Notification,
EventNotification,
Settings
}

View File

@@ -1,72 +0,0 @@
const db = require('../db')
const Sequelize = require('sequelize')
const User = require('./user')
const Event = db.define('event', {
title: Sequelize.STRING,
description: Sequelize.TEXT,
multidate: Sequelize.BOOLEAN,
start_datetime: { type: Sequelize.DATE, index: true },
end_datetime: { type: Sequelize.DATE, index: true },
image_path: Sequelize.STRING,
activitypub_id: { type: Sequelize.INTEGER, index: true },
is_visible: Sequelize.BOOLEAN
})
const Tag = db.define('tag', {
tag: { type: Sequelize.STRING, index: true, unique: true, primaryKey: true },
color: { type: Sequelize.STRING }
})
const Comment = db.define('comment', {
activitypub_id: { type: Sequelize.INTEGER, index: true },
author: Sequelize.STRING,
text: Sequelize.STRING
})
const Notification = db.define('notification', {
filters: Sequelize.JSON,
email: Sequelize.STRING,
remove_code: Sequelize.STRING,
type: {
type: Sequelize.ENUM,
values: ['mail', 'admin_email', 'mastodon']
}
})
const Place = db.define('place', {
name: { type: Sequelize.STRING, unique: true, index: true },
address: { type: Sequelize.STRING }
})
Comment.belongsTo(Event)
Event.hasMany(Comment)
Event.belongsToMany(Tag, { through: 'tagEvent' })
Tag.belongsToMany(Event, { through: 'tagEvent' })
const EventNotification = db.define('EventNotification', {
status: {
type: Sequelize.ENUM,
values: ['new', 'sent', 'error'],
defaultValue: 'new',
index: true
}
})
Event.belongsToMany(Notification, { through: EventNotification })
Notification.belongsToMany(Event, { through: EventNotification })
Event.belongsTo(User)
Event.belongsTo(Place)
User.hasMany(Event)
Place.hasMany(Event)
async function init () {
await Notification.findOrCreate({ where: { type: 'mastodon', filters: { is_visible: true } } })
await Notification.findOrCreate({ where: { type: 'admin_email', filters: { is_visible: false } } })
}
init()
module.exports = { Event, Comment, Tag, Place, Notification, EventNotification }

View File

@@ -1,37 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const db = {};
let sequelize;
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
}
fs
.readdirSync(__dirname)
.filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
})
.forEach(file => {
const model = sequelize['import'](path.join(__dirname, file));
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;

View File

@@ -1,9 +0,0 @@
const db = require('../db')
const Sequelize = require('sequelize')
const Settings = db.define('settings', {
key: { type: Sequelize.STRING, primaryKey: true, index: true },
value: Sequelize.JSON
})
module.exports = Settings

View File

@@ -1,34 +0,0 @@
const bcrypt = require('bcrypt')
const db = require('../db')
const Sequelize = require('sequelize')
const User = db.define('user', {
email: {
type: Sequelize.STRING,
unique: { msg: 'Email already exists' },
index: true,
allowNull: false
},
description: Sequelize.TEXT,
password: Sequelize.STRING,
recover_code: Sequelize.STRING,
is_admin: Sequelize.BOOLEAN,
is_active: Sequelize.BOOLEAN,
mastodon_auth: Sequelize.JSON
})
User.prototype.comparePassword = async function (pwd) {
if (!this.password) return false
const ret = await bcrypt.compare(pwd, this.password)
return ret
}
User.beforeSave(async (user, options) => {
if (user.changed('password')) {
const salt = await bcrypt.genSalt(10)
const hash = await bcrypt.hash(user.password, salt)
user.password = hash
}
})
module.exports = User

View File

@@ -1,28 +0,0 @@
const express = require('express')
const app = express()
const bodyParser = require('body-parser')
const api = require('./api')
const cors = require('cors')
const path = require('path')
const morgan = require('morgan')
const config = require('./config')
const db = require('./db')
const port = process.env.PORT || 9000
app.use(morgan('dev'))
app.set('views', path.join(__dirname, 'views'))
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())
app.use(cors())
app.use('/static', express.static(path.join(__dirname, '..', 'uploads')))
app.use('/uploads', express.static(path.join(__dirname, '..', 'uploads')))
app.use('/api', api)
app.use('/', express.static(path.join(__dirname, '..', 'client', 'dist')))
app.use('/css', express.static(path.join(__dirname, '..', 'client', 'dist', 'css')))
app.use('/js', express.static(path.join(__dirname, '..', 'client', 'dist', 'js')))
app.use('*', express.static(path.join(__dirname, '..', 'client', 'dist', 'index.html')))
app.listen(port, () => {
console.log(`[${config.title}] Started ${process.env.NODE_ENV} mode at ${config.baseurl} (api @ ${config.apiurl})\n\nCONFIG`, config)
db.sync()
})

View File

@@ -1,62 +0,0 @@
const fs = require('fs')
const os = require('os')
const path = require('path')
const crypto = require('crypto')
const mkdirp = require('mkdirp')
const sharp = require('sharp')
function getDestination (req, file, cb) {
cb(null, os.tmpdir())
}
function DiskStorage (opts) {
if (typeof opts.destination === 'string') {
mkdirp.sync(opts.destination)
this.getDestination = function ($0, $1, cb) { cb(null, opts.destination) }
} else {
this.getDestination = (opts.destination || getDestination)
}
}
DiskStorage.prototype._handleFile = function _handleFile (req, file, cb) {
var that = this
that.getDestination(req, file, function (err, destination) {
if (err) return cb(err)
const filename = crypto.randomBytes(16).toString('hex') + '.jpg'
const finalPath = path.join(destination, filename)
const thumbPath = path.join(destination, 'thumb', filename)
const outStream = fs.createWriteStream(finalPath)
const thumbStream = fs.createWriteStream(thumbPath)
const resizer = sharp().resize(800).jpeg({ quality: 80 })
const thumbnailer = sharp().resize(400).jpeg({ quality: 60 })
file.stream.pipe(thumbnailer).pipe(thumbStream)
thumbStream.on('error', e => console.log('thumbStream error ', e))
file.stream.pipe(resizer).pipe(outStream)
outStream.on('error', cb)
outStream.on('finish', function () {
cb(null, {
destination,
filename,
path: finalPath,
size: outStream.bytesWritten
})
})
})
}
DiskStorage.prototype._removeFile = function _removeFile (req, file, cb) {
var path = file.path
delete file.destination
delete file.filename
delete file.path
fs.unlink(path, cb)
}
module.exports = function (opts) {
return new DiskStorage(opts)
}

View File

@@ -1,23 +0,0 @@
doctype xml
rss(version='2.0')
channel
title #{config.title}
link #{config.baseurl}
description #{config.description}
language #{config.locale}
//- if events.length
lastBuildDate= new Date(posts[0].publishedAt).toUTCString()
each event in events
item
title= event.title
link #{config.baseurl}/event/#{event.id}
description
| <![CDATA[
| <h4>#{event.title}</h4>
| <strong>#{event.place.name} - #{event.place.address}</strong>
| #{moment(event.start_datetime).format("dddd, D MMMM HH:mm")}<br/>
| <img src="#{config.apiurl}/../uploads/#{event.image_path}"/>
| <pre>!{event.description}</pre>
| ]]>
pubDate= new Date(event.createdAt).toUTCString()
guid(isPermaLink='false') #{config.baseurl}/event/#{event.id}

View File

@@ -1,6 +0,0 @@
module.exports = {
'extends': 'standard',
"rules": {
"camelcase": 0
}
}

View File

@@ -1,14 +0,0 @@
module.exports = {
presets: [
'@vue/app' //, 'es2015', { 'modules': false }
],
plugins: [
[
'component',
{
'libraryName': 'element-ui',
'styleLibraryName': 'theme-chalk'
}
]
]
}

View File

@@ -1,68 +0,0 @@
{
"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",
"babel-plugin-component": "^1.1.1",
"bootstrap-vue": "^2.0.0-rc.13",
"dayjs": "^1.8.9",
"element-ui": "^2.6.1",
"mastodon-api": "^1.3.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-magic-grid": "^0.0.4",
"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"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,144 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="115.41461mm"
height="27.793631mm"
viewBox="0 0 115.41461 27.793631"
version="1.1"
id="svg4662"
inkscape:version="0.92.1 r15371"
sodipodi:docname="gancio_logo.svg">
<defs
id="defs4656" />
<sodipodi:namedview
id="base"
pagecolor="#acacac"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="0.5"
inkscape:cx="935.05166"
inkscape:cy="-337.87097"
inkscape:document-units="mm"
inkscape:current-layer="g4601"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1029"
inkscape:window-x="0"
inkscape:window-y="1107"
inkscape:window-maximized="1"
inkscape:pagecheckerboard="false" />
<metadata
id="metadata4659">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Livello 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(276.02814,-32.077815)">
<g
id="g4558"
transform="translate(1.3890529,0.07244553)">
<g
id="g4601"
transform="translate(0,-42.333335)">
<g
id="g4629-3"
transform="translate(-305.51837,-29.938164)">
<g
transform="translate(0,47.625002)"
id="g4603-6" />
<g
transform="translate(-3.7041668,23.812509)"
id="g4627-7">
<g
id="g4609-5"
transform="translate(2.9252966,21.389747)">
<path
d="m 124.73699,81.740247 c -0.042,-1.344482 -0.063,-2.773002 -0.063,-4.28554 0,-1.764643 0.042,-3.21416 0.12604,-4.348571 0.042,-0.378133 0.063,-0.735262 0.063,-1.071379 0,-1.17642 -0.25208,-2.037734 -0.75627,-2.583929 -0.46216,-0.58821 -1.21843,-0.882318 -2.26881,-0.882318 h -0.37814 c -2.10076,0.126047 -3.6343,0.189066 -4.60065,0.189066 -1.05038,0 -1.72263,-0.06303 -2.01673,-0.189066 0.0841,1.722621 0.12604,3.31919 0.12604,4.789718 0,0.588211 -0.021,1.176421 -0.063,1.764637 0,0.04202 -0.021,0.399144 -0.063,1.071385 -0.042,0.630226 -0.063,1.134405 -0.063,1.512544 0,1.218436 0.23109,2.20579 0.69325,2.962069 0.50418,0.714256 1.40751,1.071384 2.70998,1.071384 1.89068,0.04202 3.59229,0.06297 5.10484,0.06297 v 0 c 0.12604,0.420148 -0.86131,0.840303 -0.86131,1.260451 0.59102,-0.182249 2.25198,-0.718484 2.31083,-1.323478 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:25.39999962px;line-height:1.25;font-family:SoupLeaf;-inkscape-font-specification:'SoupLeaf, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26259452"
id="path4605-3"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cscscscscsccscccccc" />
<path
sodipodi:nodetypes="ccscscscsccccccc"
inkscape:connector-curvature="0"
id="path4607-5"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:25.39999962px;line-height:1.25;font-family:SoupLeaf;-inkscape-font-specification:'SoupLeaf, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ff0000;fill-opacity:1;stroke:none;stroke-width:0.15776582"
d="m 117.1149,61.679053 c -1.43919,0.42444 -2.03367,0.649045 -2.07229,1.182896 0.042,0.485302 0.063,1.000931 0.063,1.546894 0,0.636953 -0.042,1.526455 -0.12604,1.935931 -0.042,0.136488 -0.063,0.2654 -0.063,0.386724 0,0.424633 0.18246,0.349604 0.72145,0.453913 0.5086,0.107876 1.25327,0.272056 2.30364,0.272056 h 0.37813 c 2.10077,-0.0455 3.63431,-0.06824 4.60066,-0.06824 1.05038,0 1.37798,0.974658 1.67208,1.020149 -0.0457,-1.295385 0.31716,-2.9161 0.40768,-4.091524 0,-0.439804 -0.23108,-0.796193 -0.69325,-1.069175 -0.50419,-0.257818 -1.40752,-0.386724 -2.70998,-0.386724 -1.89069,-0.01516 -3.5923,-0.02275 -5.10484,-0.02275 v 0 c -0.41273,-0.497342 0.0725,-0.856932 0.62276,-1.16015 z" />
</g>
<path
transform="translate(0,47.625003)"
sodipodi:nodetypes="cscscccccccsssccccssccccccsccccccccc"
inkscape:connector-curvature="0"
id="path4611-6"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:25.39999962px;line-height:1.25;font-family:SoupLeaf;-inkscape-font-specification:'SoupLeaf, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26458332"
d="m 76.98359,38.242574 c -0.592667,0.338667 -0.837977,0.04028 -1.938644,0.04028 -0.804333,0 -1.672167,-0.105833 -2.6035,-0.3175 -2.328333,-0.508 -4.699,-0.762 -7.112,-0.762 -2.116667,0 -4.219443,0.597132 -4.572,0.762 -0.340875,0.03112 -1.012654,-0.341285 0.365451,-0.567012 0.26732,-0.05131 0.48816,-0.112725 0.602566,-0.206246 0.07353,-0.816118 -2.880962,-0.280435 -2.555517,1.471758 0.169333,0.635 0.275167,1.354667 0.3175,2.159 v 0.0635 0.889 c 3.641547,0.663744 8.545765,-0.506384 9.657068,-0.147837 0.787558,0.254095 0.888412,2.489721 0.385715,2.831895 -1.988477,1.353512 -8.401427,1.363785 -9.286054,0.942899 -0.844467,-0.401779 -0.418064,0.881542 -0.248729,2.024542 0.127,1.100667 0.1905,1.820333 0.1905,2.159 0.211667,1.989667 0.5715,3.788833 1.0795,5.3975 3.048,-0.211667 5.291667,-0.402167 6.731,-0.5715 3.333711,0.01832 3.530728,-0.487574 5.920619,-0.32001 1.179892,0.08273 2.336574,0.241881 3.646192,1.142358 0.715811,0.492183 0.682983,-0.789215 0.672073,-1.193451 -0.26018,-1.429378 -0.103039,-2.38471 -0.178275,-3.375398 -0.825875,-4.154467 -0.753063,-9.0021 -1.073465,-12.422778 z M 66.133782,51.469685 h -0.0635 c -0.211667,-0.423333 -0.3175,-1.312333 -0.3175,-2.667 l 0.0635,-0.0635 c 0.169333,-0.04233 0.529167,-0.105833 1.0795,-0.1905 0.211667,-0.04233 0.486833,-0.0635 0.8255,-0.0635 0.338667,0 1.672167,0.02117 1.883834,0.0635 v 0 l 0.3175,2.6035 v 0.0635 0 c -0.254,0 -1.756834,0.02117 -2.391834,0.0635 -0.677333,0.04233 -1.143,0.105834 -1.397,0.1905 z" />
<path
transform="translate(0,47.625003)"
sodipodi:nodetypes="sscscccsscccccccscccccscccsscscccs"
inkscape:connector-curvature="0"
id="path4613-2"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:25.39999962px;line-height:1.25;font-family:SoupLeaf;-inkscape-font-specification:'SoupLeaf, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26458332"
d="m 97.168085,54.221352 c 0.838348,-1.067123 0.6985,-4.148667 0.6985,-6.477 0,-2.328333 -0.3175,-4.3815 -0.9525,-6.1595 -1.016,-3.090333 -2.116667,-4.6355 -3.302,-4.6355 -0.127,0 -0.338667,0.04233 -0.635,0.127 -0.254,0.04233 -0.402167,0.0635 -0.4445,0.0635 -0.169333,0.04233 -0.5715,0.127 -1.2065,0.254 -0.635,0.08467 -1.248833,0.169333 -1.8415,0.254 -0.592667,0.08467 -1.058333,0.127 -1.397,0.127 -0.296333,0 -0.529167,-0.02117 -0.6985,-0.0635 -0.677333,-0.254 -1.121833,-0.719667 -1.3335,-1.397 -0.09681,-0.188828 -0.131976,-0.218944 -0.381,-0.1905 -0.635,0.127 -2.0955,0.4445 -4.3815,0.9525 -3.623469,-0.381 -2.513622,8.534263 -2.286,13.589 l 0.1905,4.699 7.874,-0.127 c 0.254,-1.481667 0.381,-3.259667 0.381,-5.334 0,-0.762 -0.02117,-1.439333 -0.0635,-2.032 -0.169333,-2.751667 -0.338667,-4.529667 -0.508,-5.334 v 0 l 0.0635,-0.0635 v 0 c 0.381,-0.127 0.762,-0.1905 1.143,-0.1905 0.465667,0 0.762,0.148167 0.889,0.4445 0.423333,0.931333 0.635,3.132667 0.635,6.604 -0.03639,0.326737 -0.0635,0.552493 -0.0635,0.889 0,0.296333 -0.02117,0.719667 -0.0635,1.27 -0.04233,0.550333 -0.0635,0.931333 -0.0635,1.143 0,1.058333 0.5715,1.651 1.7145,1.778 0.592667,0.04233 1.143,0.0635 1.651,0.0635 0.635,0 1.312333,-0.02117 2.032,-0.0635 l 0.748355,-0.08851 c 1.1219,-0.0845 -0.343517,0.755441 -0.343517,0.755441 0,0 1.075474,0.248947 1.944662,-0.857431 z" />
<path
transform="translate(0,47.625003)"
inkscape:connector-curvature="0"
id="path4615-9"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:25.39999962px;line-height:1.25;font-family:SoupLeaf;-inkscape-font-specification:'SoupLeaf, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26458332"
d="m 144.74231,37.865848 c 0.67677,0.05658 1.21623,0.713304 0.7936,0.755671 v 0 c -1.94733,0 -3.81,-0.08467 -5.588,-0.254 -0.46567,-0.04233 -1.56633,-0.1905 -3.302,-0.4445 -1.69333,-0.254 -2.96333,-0.381 -3.81,-0.381 -2.286,0 -3.59833,0.804333 -3.937,2.413 v 0 0 0 0 0 c -0.381,0.635 -0.0423,2.518833 -0.0423,5.6515 v 1.397 c 0.127,5.461 0.29633,8.403167 1.56633,8.8265 0.127,0.04233 0.254,0.0635 0.381,0.0635 0.127,0 0.78317,-0.148167 1.9685,-0.4445 1.18533,-0.338667 2.43417,-0.508 3.7465,-0.508 0.381,0 0.86783,0.02117 1.4605,0.0635 0.508,0.04233 1.16417,0.105833 1.9685,0.1905 0.80433,0.08467 1.48167,0.148167 2.032,0.1905 0.59267,0.04233 1.10067,0.0635 1.524,0.0635 1.94733,0 2.94217,-0.592667 2.9845,-1.778 0.0423,-0.465667 0.0635,-0.889 0.0635,-1.27 0,-0.677333 -0.0423,-1.883833 -0.127,-3.6195 -0.0423,-1.735667 -0.0635,-3.005667 -0.0635,-3.81 0,-1.227667 0.0423,-2.074333 0.127,-2.54 0.16933,-0.762 0.27517,-1.397 0.3175,-1.905 v -0.0635 c 1.04195,-2.143001 -0.15974,-2.071757 -0.58855,-2.447681 -3.29195,-1.671465 -3.46421,-0.315774 -1.47505,-0.14949 z m -6.3184,13.455671 h -0.3175 c -0.635,0.04233 -1.0795,0.08467 -1.3335,0.127 h -0.0635 v -2.794 -0.0635 0 c 0.46567,-0.127 0.97367,-0.1905 1.524,-0.1905 h 0.127 c 0.59267,0.04233 0.97367,0.0635 1.143,0.0635 v 0 0.0635 0 c 0.0847,0.635 0.127,1.227667 0.127,1.778 0,0.508 -0.0423,0.867833 -0.127,1.0795 v 0.0635 l -0.0635,-0.0635 v 0 c -0.16933,-0.04233 -0.508,-0.0635 -1.016,-0.0635 z"
sodipodi:nodetypes="sccccsccccccscsscscscscscsccccssccccccscccccsccccs" />
<path
transform="translate(0,47.625003)"
sodipodi:nodetypes="ccccscsccccccscccccsscsscccscccc"
inkscape:connector-curvature="0"
id="path4617-1"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:25.39999962px;line-height:1.25;font-family:SoupLeaf;-inkscape-font-specification:'SoupLeaf, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26386419"
d="m 113.41865,34.862971 c 0.27611,0.77181 1.38276,1.788884 1.57103,3.748148 -0.76664,-0.111392 -0.17479,-0.05401 -0.7371,-0.04986 -0.84437,0 -1.77317,-0.105539 -2.7864,-0.316636 -2.61753,-0.633273 -5.19285,-0.94991 -7.72595,-0.94991 -2.82864,0 -4.45404,0.464401 -4.87622,1.393201 -0.33774,0.717713 -0.50662,1.878717 -0.50662,3.48301 0,0.759928 0.0422,1.604295 0.12665,2.533085 0.0422,0.886585 0.12665,1.857613 0.25332,2.913066 0.0845,1.01324 0.14776,1.73095 0.18997,2.15313 0.12666,2.026474 0.42218,3.862968 0.88659,5.509481 3.30386,-0.09618 6.42529,-0.46272 9.87909,-0.759922 0.88658,0 1.70984,0.04222 2.46977,0.126649 0.75992,0.04222 1.41431,0.06327 1.96314,0.06327 0.92881,0 1.17291,-0.08444 1.46844,-0.253312 0.33775,-0.211093 0.61216,-0.591054 0.82326,-1.139902 v 0 l 0.0633,-0.06327 c 0,-0.675491 0.0211,-1.350988 0.0633,-2.026481 -1.85761,-0.506616 -4.02265,-0.759921 -7.23123,-0.759921 h -1.32987 c -0.42219,0 -0.78104,0.02105 -1.07657,0.06327 h -0.44329 c -0.16888,0 -0.25331,-0.189981 -0.25331,-0.569941 0,-0.464405 -0.0172,-1.655752 0.27837,-2.58455 1.29317,-0.233942 1.50635,-0.161062 1.7481,-0.159634 0.33775,0 2.34312,0.105539 6.0161,0.316629 h 0.12666 c 0.88658,0 1.51986,0.2111 1.89982,-0.422173 0.37997,-0.675498 0.59106,-1.350989 0.63328,-2.026481 0,-1.055453 -0.0225,-2.917481 -0.14917,-5.492795 -0.49568,-2.772882 -1.82493,-3.79722 -3.34452,-4.728181 z" />
<g
transform="translate(0,1.0583333)"
id="g4625-2">
<path
id="path4619-7"
transform="scale(0.26458333)"
d="m 214.45117,317.70508 c -11.17785,0.12645 -1.97461,2.4707 -1.97461,2.4707 0.5209,-0.0102 2.12086,-0.88425 2.60352,0.20313 0.26759,0.60286 -0.0748,1.82552 -1.45899,1.83203 h -5.51953 c -4.96,0 -9.59992,-0.16046 -13.91992,-0.48047 -11.68,-1.12 -19.04008,-1.67969 -22.08008,-1.67969 -11.51999,0 -17.68046,2.55969 -18.48047,7.67969 -0.96,4.96 -1.43945,13.04023 -1.43945,20.24023 0,4.64 0.2407,9.27992 0.7207,13.91992 1.6,11.52001 0.87465,10.20499 4.56055,12.88086 5.95738,4.32492 14.39813,0.56055 22.07813,0.56055 h 0.24023 c 3.52,0 12.48086,-1.52055 14.88086,-0.56055 2.40375,0.40125 3.76805,-0.0327 4.56055,2.63868 1.01305,2.48837 1.88809,11.33815 1.19922,11.92187 -6.11795,3.16753 -15.65105,2.86885 -21.59961,3.19922 -2.72001,0.15999 -5.04094,0.24023 -6.96094,0.24023 -2.146,0 -3.82486,-0.13999 -5.15235,-0.3164 v 10.4043 c 4.6584,1.32321 9.90901,2.30468 15.89063,2.30468 4.32,0 8.3825,-0.71367 12.0625,-1.51367 10.24,-2.08 19.35937,-6.63969 27.35937,-13.67969 -0.63999,-6.72 -0.95898,-19.6789 -0.95898,-26.8789 0,-3.68 0.0803,-7.20055 0.24023,-10.56055 0.32002,-3.52 0.47852,-7.52 0.47852,-12 -0.0979,-5.3278 1.06159,-13.42096 -0.71875,-17.59961 -0.63059,-1.48005 -1.53866,-2.43226 -2.11523,-3.26758 -0.49509,-0.71725 -1.23668,-1.43299 -1.98047,-1.95312 -0.93513,-0.0126 -1.77044,-0.0143 -2.51563,-0.006 z m -18.58984,31.22656 c 0.8,1.6 1.28141,6.96008 1.4414,12.08008 v 0.24023 c -0.63999,0.15999 -2.00008,0.39875 -4.08007,0.71875 -1.44,0.15999 -9.55588,0.24024 -8.32032,0.24024 h -1.67968 -0.24024 -0.24023 l -1.43946,-11.83985 0.23829,-0.24023 c 1.28,-0.15999 6.96101,-0.32048 9.04101,-0.48047 2.72,-0.15999 4.4793,-0.39875 5.2793,-0.71875 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:25.39999962px;line-height:1.25;font-family:SoupLeaf;-inkscape-font-specification:'SoupLeaf, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.99999994"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccscscscassccccsccsccscccccccccccsccccccc" />
<g
style="fill:#ff0000"
id="g4623-0"
transform="matrix(-0.05837501,0.25245606,-0.25245606,-0.05837501,111.30736,132.11782)">
<path
sodipodi:nodetypes="cccsccccssccccccccscsccccscccc"
id="path4621-9"
transform="matrix(-0.23003668,-0.99484615,0.99484615,-0.23003668,-399.99545,533.38826)"
d="m 164.75391,300.12109 c -3.24373,-0.0303 -6.11471,0.11966 -9.08594,0.44532 -18.35072,2.01124 -28.09411,10.14145 -33.32227,27.80273 -2.6272,8.875 -2.82125,22.83155 -0.63867,32.31445 1.58264,6.87629 3.85109,12.39088 7.49609,18.22266 3.58528,5.73621 7.79754,10.08127 13.26368,13.68359 4.77225,3.14504 12.34952,7.019 19.76953,9.14649 v -10.13867 c -3.53307,-0.904 -6.47332,-1.86856 -8.15821,-2.91993 -7.43035,-4.63653 -11.84095,-8.9198 -15.6914,-14.55859 -4.8805,-7.14724 -7.01189,-19.5916 -6.74805,-28.5293 0.0988,-3.34042 1.62817,-15.4335 2.95703,-18.24804 2.69245,-5.70267 4.69314,-9.15557 9.09375,-11.87305 6.5257,-3.76431 13.56281,-6.88759 28.22657,-5.90625 0.11861,0.0736 -4.60161,3.38467 -6.08008,4.93945 -2.3994,2.52323 -3.10156,3.78516 -3.10156,3.78516 -0.0161,0.0996 4.54165,-1.93431 6.35546,-2.87305 4.45264,-2.30444 8.02967,-4.57269 9.46875,-5.07422 0.74639,-0.11144 7.60841,-1.45597 15.38282,-2.23242 22.78632,-2.27574 24.21952,-2.50438 20.27343,-3.23047 -7.74703,-1.42545 -16.55392,-2.52304 -24.16992,-3.01367 -3.02668,-0.19498 -9.31417,-0.69271 -13.97265,-1.10547 -4.45786,-0.39497 -8.07464,-0.60645 -11.31836,-0.63672 z m -12.52149,4.77344 c 1.24553,-0.031 2.52435,0.0214 3.83203,0.16211 1.97529,0.21249 1.83203,0.58511 -0.33007,0.85547 -11.18977,1.39922 -20.74431,6.18752 -25.23047,14.72656 -0.43702,0.83185 -0.8817,1.4934 -0.98829,1.46875 -0.32103,-0.0742 0.58228,-3.32965 1.42188,-5.125 3.51572,-7.51775 12.57624,-11.87076 21.29492,-12.08789 z"
style="fill:#ff0000;stroke-width:0.25911719"
inkscape:connector-curvature="0" />
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,17 +0,0 @@
<!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>

View File

@@ -1,83 +0,0 @@
<template lang='pug'>
#app
Nav
Home
transition(name="fade" mode="out-in")
router-view(name='modal')
</template>
<script>
import moment from 'dayjs'
import api from '@/api'
import { mapActions } 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 Home from '@/components/Home'
import Nav from '@/components/Nav'
export default {
name: 'App',
mounted () {
this.updateMeta()
},
methods: mapActions(['updateMeta']),
components: { Nav, Register, Login, Home, Settings, newEvent, eventDetail },
}
</script>
<style>
#logo {
max-height: 40px;
}
.navbar-brand {
padding: 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;
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>

View File

@@ -1,80 +0,0 @@
import axios from 'axios'
import store from './store'
const api = axios.create({
baseURL: process.env.NODE_ENV === 'development' ? 'http://localhost:9000/api' : '/api',
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(res => res.data)
.catch(e => {
if (e.response.status === 403) {
store.commit('logout')
return false
}
throw e.response && e.response.data &&
e.response.data.errors && e.response.data.errors[0].message
})
}
function post (path, data) {
return api.post(path, data, { headers: { 'x-access-token': store.state.token } })
.then(res => res.data)
.catch(e => {
if (e.response.status === 403) {
store.commit('logout')
return false
}
throw e.response && e.response.data &&
e.response.data.errors && e.response.data.errors[0].message
})
}
function put (path, data) {
return api.put(path, data, { headers: { 'x-access-token': store.state.token } })
.then(ret => ret.data)
}
function del (path) {
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),
// password recovery
forgotPassword: email => post('/user/recover', { email }),
checkRecoverCode: recover_code => post('/user/check_recover_code', { recover_code }),
recoverPassword: (recover_code, password) => post('/user/recover_password', { recover_code, password }),
getAllEvents: (month, year) => get(`/event/${year}/${month}`),
getUnconfirmedEvents: () => get('/event/unconfirmed'),
confirmEvent: id => get(`/event/confirm/${id}`),
unconfirmEvent: id => get(`/event/unconfirm/${id}`),
addNotification: notification => post('/event/notification', notification),
delNotification: code => del(`/event/notification/${code}`),
addEvent: event => post('/user/event', event),
updateEvent: event => put('/user/event', event),
updatePlace: place => put('/place', place),
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),
getAdminSettings: () => get('/settings')
// setAdminSetting: (key, value) => post('/settings', { key, value })
}

View File

@@ -1,30 +0,0 @@
/* html, body {
max-height: 100%;
overflow: hidden;
} */
pre {
display: inline-block;
color: unset;
white-space: pre-line;
font-family: unset;
font-size: 1em;
}
.el-tag {
color: white;
font-size: 15px;
font-family: Lato;
}
.el-tag--info {
color: #909399
}
.el-tag:hover {
cursor: pointer;
}
a, a:hover {
color: red;
}

View File

@@ -1,34 +0,0 @@
<template lang="pug">
b-modal(hide-footer @hidden='$router.replace("/")' :title='$t("About")'
:visible='true' size='lg')
h5 Chi siamo
p.
Gancio e' un progetto dell'<a href='https://autistici.org/underscore'>underscore hacklab</a> e uno dei
servizi di <a href='https://cisti.org'>cisti.org</a>.
h5 Cos'è gancio?
p.
Uno strumento di condivisione di eventi per comunità radicali.
Dentro gancio puoi trovare e inserire eventi.
Gancio, come tutto <a href='https://cisti.org'>cisti.org</a> è uno strumento
antisessista, antirazzista, antifascista e anticapitalista, riflettici quando
pubblichi un evento.
h5 Ok, ma cosa vuol dire gancio?
blockquote.
Se vieni a Torino e dici: "ehi, ci diamo un gancio alle 8?" nessuno si presenterà con i guantoni per fare a mazzate.
Darsi un gancio vuol dire beccarsi alle ore X in un posto Y
p
small A: a che ora è il gancio in radio per andare al presidio?
p
small B: non so ma domani non posso venire, ho gia' un gancio per caricare il bar.
h5 Contatti
p.
Hai scritto una nuova interfaccia per gancio? Vuoi aprire un nuovo nodo di gancio nella tua città?
C'è qualcosa che vorresti migliorare? Per contribuire i sorgenti sono liberi e disponibili
<a href='https://git.lattuga.net/cisti/gancio'>qui</a>. Aiuti e suggerimenti sono sempre benvenuti, puoi scriverci
su underscore chicciola autistici.org
</template>

View File

@@ -1,196 +0,0 @@
<template lang="pug">
b-modal(hide-footer @hidden='$router.replace("/")' :title='$t("Admin")'
:visible='true' size='lg')
el-tabs(tabPosition='left' v-model='tab')
//- USERS
el-tab-pane.pt-1
template(slot='label')
v-icon(name='users')
span.ml-1 {{$t('Users')}}
el-table(:data='paginatedUsers' small)
el-table-column(label='Email')
template(slot-scope='data')
el-popover(trigger='hover' :content='data.row.description' width='400')
span(slot='reference') {{data.row.email}}
el-table-column(label='Azioni')
template(slot-scope='data')
el-button.mr-1(size='mini'
:type='data.row.is_active?"warning":"success"'
@click='toggle(data.row)') {{data.row.is_active?$t('Deactivate'):$t('Activate')}}
el-button(size='mini'
:type='data.row.is_admin?"danger":"warning"'
@click='toggleAdmin(data.row)') {{data.row.is_admin?$t('Remove Admin'):$t('Admin')}}
el-pagination(:page-size='perPage' :currentPage.sync='userPage' :total='users.length')
//- PLACES
el-tab-pane.pt-1
template(slot='label')
v-icon(name='map-marker-alt')
span.ml-1 {{$t('Places')}}
p {{$t('admin_place_explanation')}}
el-form.mb-2(:inline='true' label-width='120px')
el-form-item(:label="$t('Name')")
el-input.mr-1(:placeholder='$t("Name")' v-model='place.name')
el-form-item(:label="$t('Address')")
el-input.mr-1(:placeholder='$t("Address")' v-model='place.address')
el-button(variant='primary' @click='savePlace') {{$t('Save')}}
b-table(selectable :items='places' :fields='placeFields' striped hover
small selectedVariant='success' primary-key='id'
select-mode="single" @row-selected='placeSelected'
:per-page='perPage' :current-page='placePage')
el-pagination(:page-size='perPage' :currentPage.sync='placePage' :total='places.length')
//- EVENTS
el-tab-pane.pt-1
template(slot='label')
v-icon(name='calendar')
span.ml-1 {{$t('Events')}}
p {{$t('event_confirm_explanation')}}
el-table(:data='paginatedEvents' small primary-key='id' v-loading='loading')
el-table-column(:label='$t("Name")')
template(slot-scope='data') {{data.row.title}}
el-table-column(:label='$t("Where")')
template(slot-scope='data') {{data.row.place.name}}
el-table-column(:label='$t("Confirm")')
template(slot-scope='data')
el-button(type='primary' @click='confirm(data.row.id)' size='mini') {{$t('Confirm')}}
el-button(type='success' @click='preview(data.row.id)' size='mini') {{$t('Preview')}}
el-pagination(:page-size='perPage' :currentPage.sync='eventPage' :total='events.length')
//- TAGS
el-tab-pane.pt-1
template(slot='label')
v-icon(name='tag')
span {{$t('Tags')}}
p {{$t('admin_tag_explanation')}}
el-tag(v-if='tag.tag' :color='tag.color || "grey"' size='mini') {{tag.tag}}
el-form(:inline='true' label-width='120px')
el-form-item(:label="$t('Color')")
el-color-picker(v-model='tag.color' @change='updateColor')
el-table(:data='paginatedTags' striped small hover
highlight-current-row @current-change="tagSelected")
el-table-column(label='Tag')
template(slot-scope='data')
el-tag(:color='data.row.color || "grey"' size='mini') {{data.row.tag}}
el-pagination(:page-size='perPage' :currentPage.sync='tagPage' :total='tags.length')
//- SETTINGS
el-tab-pane.pt-1
template(slot='label')
v-icon(name='tools')
span {{$t('Settings')}}
el-form(inline)
span {{$t('admin_mastodon_explanation')}}
el-input(v-model="mastodon_instance")
span(slot='prepend') {{$t('Mastodon instance')}}
el-button(slot='append' @click='associate' variant='success' type='success') {{$t('Associate')}}
</template>
<script>
import { mapState } from 'vuex'
import api from '@/api'
import { Message } from 'element-ui'
export default {
name: 'Admin',
data () {
return {
perPage: 10,
users: [],
userFields: ['email', 'action'],
placeFields: ['name', 'address'],
placePage: 1,
userPage: 1,
eventPage: 1,
tagPage: 1,
tagFields: ['tag', 'color'],
description: '',
place: {name: '', address: '' },
tag: {name: '', color: ''},
events: [],
loading: false,
mastodon_instance: '',
settings: {},
tab: "0",
}
},
async mounted () {
const code = this.$route.query.code
if (code) {
this.tab = "4"
const instance = await api.setCode({code, is_admin: true})
}
this.users = await api.getUsers()
this.events = await api.getUnconfirmedEvents()
this.settings = await api.getAdminSettings()
this.mastodon_instance = this.settings.mastodon_auth && this.settings.mastodon_auth.instance
},
computed: {
...mapState(['tags', 'places']),
paginatedEvents () {
return this.events.slice((this.eventPage-1) * this.perPage,
this.eventPage * this.perPage)
},
paginatedTags () {
return this.tags.slice((this.tagPage-1) * this.perPage,
this.tagPage * this.perPage)
},
paginatedUsers () {
return this.users.slice((this.userPage-1) * this.perPage,
this.userPage * this.perPage)
}
},
methods: {
placeSelected (items) {
if (items.length === 0 ) {
this.place.name = this.place.address = ''
return
}
const item = items[0]
this.place.name = item.name
this.place.address = item.address
this.place.id = item.id
},
tagSelected (tag) {
this.tag = tag
},
async savePlace () {
const place = await api.updatePlace(this.place)
},
async toggle(user) {
user.is_active = !user.is_active
const newuser = await api.updateUser(user)
},
async toggleAdmin(user) {
user.is_admin = !user.is_admin
const newuser = await api.updateUser(user)
},
async updateColor () {
const newTag = await api.updateTag(this.tag)
},
preview (id) {
this.$router.push(`/event/${id}`)
},
async associate () {
if (!this.mastodon_instance) return
const url = await api.getAuthURL({instance: this.mastodon_instance, admin: true})
setTimeout( () => window.location.href=url, 100);
},
async confirm (id) {
try {
this.loading = true
await api.confirmEvent(id)
this.loading = false
Message({
message: this.$t('event_confirmed'),
type: 'success'
})
this.events = this.events.filter(e => e.id !== id)
} catch (e) {
}
}
}
}
</script>

View File

@@ -1,93 +0,0 @@
<template lang="pug">
v-calendar#calendar.card(
show-caps
:popover-expanded='true'
: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 'dayjs'
import { intersection } from 'lodash'
export default {
name: 'Calendar',
filters,
data () {
const month = moment().month()+1
const year = moment().year()
return {
page: { month, year},
}
},
async mounted () {
await 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'
}
}
let color = event.tags.length && event.tags[0].color ? event.tags[0].color : 'rgba(170,170,250,0.7)'
if (event.past) 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: {
filteredEvents () {
return this.$store.getters.filteredEvents
},
...mapState(['events', 'filters', 'user', 'logged']),
attributes () {
return [
{ key: 'todaly', dates: new Date(),
highlight: {
backgroundColor: '#aaffaa'
},
popover: {label: this.$t('Today')}
},
...this.filteredEvents.map(this.eventToAttribute)
]
}
}
}
</script>
<style>
#calendar {
margin-bottom: 0em;
margin-top: 0.3em;
}
#calendar a {
color: blue;
}
</style>

View File

@@ -1,39 +0,0 @@
<template lang="pug">
b-modal(hide-footer @hidden='$router.replace("/")' :title='$t("Remove notification")'
:visible='true' size='sm' ref='modal')
div( v-loading='loading')
p Vuoi eliminare le notifiche?
el-button.float-right(type='success' @click='remove') {{$t('Yes')}}
el-button.float-right.mr-1(type='danger' @click='$refs.modal.hide()') {{$t('No')}}
</template>
<script>
import api from '@/api'
import { Message } from 'element-ui';
export default {
name: 'DelNotification',
data () {
return {
code: '',
loading: false
}
},
mounted () {
this.code = this.$route.params.code
},
methods: {
async remove () {
this.loading = true
try {
await api.delNotification(this.code)
Message({message: this.$t('Email notification removed'), type: 'success'})
} catch(e) {
Message({message: this.$t('Error removing email notification'), type: 'error'})
}
this.$refs.modal.hide()
this.loading = false
}
}
}
</script>

View File

@@ -1,65 +0,0 @@
<template lang="pug">
b-card(bg-variant='dark' text-variant='white' :class="{ withImg: event.image_path ? true : false }"
@click='$router.push("/event/" + event.id)'
:img-src='imgPath')
strong {{event.title}}
div <v-icon name='clock'/> {{event.start_datetime|datetime}}
span <v-icon name='map-marker-alt'/> {{event.place.name}}
br
el-tag.mr-1(:color='tag.color || "grey"' v-for='tag in event.tags' :key='tag.tag'
size='small' @click.stop='addSearchTag(tag)') {{tag.tag}}
</template>
<script>
import { mapState, mapActions } from 'vuex';
import api from '@/api'
import filters from '@/filters'
export default {
props: ['event'],
computed: {
...mapState(['user']),
imgPath () {
return this.event.image_path && process.env.VUE_APP_API + '/uploads/thumb/' + 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: ''
} */
.el-card {
border: none;
}
.el-card img {
width: 100%;
}
.card-columns .card {
margin-top: 0.2em;
margin-bottom: 0em;
}
.card-img {
height: 180px;
object-fit: cover;
}
</style>

View File

@@ -1,122 +0,0 @@
<template lang="pug">
b-modal#eventDetail(ref='eventDetail' hide-body hide-header hide-footer @hidden='$router.replace("/")' size='lg' :visible='true')
b-card(no-body, :img-src='imgPath' v-loading='loading')
el-button.close_button(circle icon='el-icon-close' type='success'
@click='$refs.eventDetail.hide()')
b-card-header
h3 {{event.title}}
v-icon(name='clock')
span {{event.start_datetime|datetime}}
br
v-icon(name='map-marker-alt')
span {{event.place.name}} - {{event.place.address}}
br
b-card-body(v-if='event.description || event.tags')
pre(v-html='event.description')
br
el-tag.mr-1(:color='tag.color || "grey"' v-for='tag in event.tags'
size='mini' :key='tag.tag') {{tag.tag}}
.ml-auto(v-if='mine')
hr
el-button(v-if='event.is_visible' plain type='warning' @click.prevents='toggle' icon='el-icon-view') {{$t('Unconfirm')}}
el-button(v-else plain type='success' @click.prevents='toggle' icon='el-icon-view') {{$t('Confirm')}}
el-button(plain type='danger' @click.prevent='remove' icon='el-icon-remove') {{$t('Remove')}}
el-button(plain type='primary' @click='$router.replace("/edit/"+event.id)') <v-icon color='orange' name='edit'/> {{$t('Edit')}}
//- COMMENTS ...
//- b-navbar(type="dark" variant="dark" toggleable='lg')
//- template(slot='footer')
//- b-navbar-nav
//- b-button(variant='success') {{$t('Share')}} <v-icon name='share'/>
//- b-nav-item( {{$t('')}})
//- 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'
export default {
name: 'EventDetail',
computed: {
...mapState(['user']),
imgPath () {
return this.event.image_path && process.env.VUE_APP_API + '/uploads/' + this.event.image_path
},
mine () {
return this.event.userId === this.user.id || this.user.is_admin
}
},
data () {
return {
event: { comments: [], place: {}, title: ''},
id: null,
loading: true,
}
},
mounted () {
this.id = this.$route.params.id
this.load()
},
filters,
methods: {
...mapActions(['delEvent']),
async load () {
const event = await api.getEvent(this.id)
this.event = event
this.loading = false
},
async remove () {
await api.delEvent(this.event.id)
this.delEvent(this.event.id)
this.$refs.eventDetail.hide()
},
async toggle () {
try {
if (this.event.is_visible) {
await api.unconfirmEvent(this.id)
this.event.is_visible = false
} else {
await api.confirmEvent(this.id)
this.event.is_visible = true
}
} catch (e) {
}
}
}
}
</script>
<style>
#eventDetail .modal-body {
padding: 0px;
width: 100%;
}
#eventDetail .close_button:hover {
background-color: rgba(200, 100, 100, 0.4);
}
#eventDetail .card {
border: 0px;
}
#eventDetail .close_button {
background-color: rgba(100, 100, 100, 0.4);
color: red;
font-size: 20px;
border: none;
position: absolute;
top: 10px;
right: 10px;
}
</style>

View File

@@ -1,133 +0,0 @@
<template lang="pug">
b-modal(ref='modal' @hidden='$router.replace("/")'
:title='$t("Export")' :visible='true' size='lg' hide-footer)
p {{$t('export_intro')}}
li(v-if='filters.tags.length') {{$t('Tags')}}:
el-tag.ml-1(color='#409EFF' size='mini' v-for='tag in filters.tags' :key='tag.tag') {{tag}}
li(v-if='filters.places.length') {{$t('Places')}}:
el-tag.ml-1(color='#409EFF' size='mini' v-for='place in filters.places' :key='place.id') {{place}}
el-tabs.mt-2(tabPosition='left' v-model='type')
el-tab-pane.pt-1(label='email' name='email')
p(v-html='$t(`export_email_explanation`)')
el-form(@submit.native.prevent)
//- el-switch(v-model='notification.notify_on_add' :active-text="$t('notify_on_insert')")
//- br
//- el-switch.mt-2(v-model='notification.send_notification' :active-text="$t('send_notification')")
el-input.mt-2(v-model='notification.email' :placeholder="$t('Insert your address')" ref='email')
el-button.mt-2.float-right(native-type= 'submit' type='success' @click='add_notification') {{$t('Send')}}
el-tab-pane.pt-1(label='feed rss' name='feed')
span(v-html='$t(`export_feed_explanation`)')
el-input(v-model='link')
el-button(slot='append' plain type="primary" icon='el-icon-document' v-clipboard:copy="link") {{$t("Copy")}}
el-tab-pane.pt-1(label='ics/ical' name='ics')
p(v-html='$t(`export_ical_explanation`)')
el-input(v-model='link')
el-button(slot='append' plain type="primary" icon='el-icon-document' v-clipboard:copy="link") {{$t("Copy")}}
el-tab-pane.pt-1(label='list' name='list')
p(v-html='$t(`export_list_explanation`)')
el-card.mb-1(no-body header='Eventi')
b-list-group#list(flush)
b-list-group-item.flex-column.align-items-start(v-for="event in filteredEvents" :key='event.id'
:to='`/event/${event.id}`')
//- b-media
img(v-if='event.image_path' slot="aside" :src="imgPath(event)" alt="Meia Aside" style='max-height: 60px')
small.float-right {{event.start_datetime|datetime}}
strong.mb-1 {{event.title}}
br
small.float-right {{event.place.name}}
el-tag.mr-1(:color='tag.color || "grey"' size='mini' v-for='tag in event.tags' :key='tag.tag') {{tag.tag}}
el-input.mb-1(type='textarea' v-model='script')
el-button.float-right(plain type="primary" icon='el-icon-document' v-clipboard:copy="script") Copy
el-tab-pane.pt-1(label='calendar' name='calendar')
p(v-html='$t(`export_calendar_explanation`)')
Calendar.mb-1
el-input.mb-1(type='textarea' v-model='script')
el-button.float-right(plain type="primary" icon='el-icon-document' v-clipboard:copy="script") Copy
</template>
<script>
import { mapState } from 'vuex'
import path from 'path'
import filters from '../filters'
import Calendar from '@/components/Calendar'
import {intersection} from 'lodash'
import api from '@/api'
import { Message } from 'element-ui'
export default {
name: 'Export',
components: { Calendar },
data () {
return {
type: 'email',
link: '',
export_list: true,
script: `<iframe>Ti piacerebbe</iframe>`,
notification: { email: '' },
}
},
filters,
mounted () {
this.link = this.loadLink()
},
watch: {
type (value) {
this.link = this.loadLink()
}
},
methods: {
async add_notification () {
if (!this.notification.email){
Message({message:'Inserisci una mail', type: 'error'})
return this.$refs.email.focus()
}
await api.addNotification({ ...this.notification, filters: this.filters})
this.$refs.modal.hide()
Message({message: this.$t('email_notification_activated'), type: 'success'})
},
loadLink () {
const tags = this.filters.tags.join(',')
const places = this.filters.places.join(',')
let query = ''
if (tags || places) {
query = '?'
if (tags) {
query += 'tags=' + tags
if (places) { query += '&places=' + places }
} else {
query += 'places=' + places
}
}
return `${process.env.VUE_APP_API}/api/export/${this.type}${query}`
},
imgPath (event) {
return event.image_path && event.image_path
},
},
computed: {
...mapState(['filters', 'user', 'logged', 'events']),
filteredEvents () {
return this.$store.getters.filteredEvents.filter(e => !e.past)
},
showLink () {
return (['feed', 'ics'].indexOf(this.type)>-1)
},
}
}
</script>
<style>
#list {
max-height: 400px;
overflow-y: scroll;
}
</style>

View File

@@ -1,92 +0,0 @@
<template lang="pug">
magic-grid(:animate="false" useMin :gap=5 :maxCols=4
:maxColWidth='400' ref='magicgrid')
div.mt-1.item
Search#search
Calendar
Event.item.mt-1(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'
import moment from 'dayjs'
import Search from '@/components/Search'
export default {
name: 'Home',
components: { Event, Calendar, Search },
watch: {
filteredEvents () {
this.$nextTick(this.$refs.magicgrid.positionItems)
}
},
computed: {
...mapState(['events', 'filters']),
filteredEvents () {
return this.$store.getters.filteredEvents
.filter(e => !e.past)
.sort((a, b) => { a.start_datetime > b.start_datetime})
}
}
}
</script>
<style>
#search {
display: inline-flex;
}
.item {
width: 100%;
max-width: 400px;
margin-top: 4px;
position: absolute;
cursor: pointer;
}
.card-columns {
column-count: 1;
column-gap: 0.2em;
}
@media (min-width: 576px) {
.container {
max-width: none;
}
.card-columns {
column-count: 2;
}
}
@media (min-width: 950px) {
.container {
max-width: 1400px;
}
.card-columns {
column-count: 3;
}
}
/* .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>

View File

@@ -1,66 +0,0 @@
<template lang='pug'>
b-modal(@shown="$refs.email.focus()" :title='$t("Login")' hide-footer
@hidden='$router.replace("/")' :visible='true' ref='modal')
el-form(v-loading='loading')
p(v-html="$t('login_explanation')")
el-input.mb-2(v-model='email' type='email' :placeholder='$t("Email")' autocomplete='email' ref='email')
v-icon(name='user' slot='prepend')
el-input.mb-1(v-model='password' @keyup.enter.native="submit" type='password' :placeholder='$t("Password")')
v-icon(name="lock" slot='prepend')
el-button.mr-1(plain type="success" @click='submit') {{$t('Login')}}
router-link(to='/register')
el-button.mt-1(plain type="primary") {{$t('Not registered?')}}
a.float-right(href='#' @click='forgot') {{$t('Forgot password?')}}
</template>
<script>
import api from '@/api'
import { mapActions } from 'vuex'
import { Message } from 'element-ui'
export default {
name: 'Login',
data () {
return {
password: '',
email: '',
loading: false
}
},
methods: {
...mapActions(['login']),
async forgot () {
if (!this.email) {
Message({ message: this.$t('Insert your email'), type: 'error' })
this.$refs.email.focus()
return
}
this.loading = true
await api.forgotPassword(this.email)
this.loading = false
Message({ message: this.$t('Check your email!'), type: 'success' })
},
async submit (e) {
e.preventDefault()
try {
this.loading = true
const user = await api.login(this.email, this.password)
this.loading = false
if (!user) {
Message({ message: this.$t('Login error'), type: 'error' })
return;
}
this.login(user)
Message({ message: this.$t('Logged'), type: 'success' })
} catch (e) {
Message({ message: this.$t('Login error'), type: 'error' })
this.loading = false
return
}
this.email = this.password = ''
this.$refs.modal.hide()
}
}
}
</script>

View File

@@ -1,50 +0,0 @@
<template lang="pug">
b-navbar(type="dark" variant="dark" toggleable='md')
b-navbar-toggle(target='nav_collapse')
b-navbar-brand(to='/') <img id='logo' src='gancio_logo.svg'/>
b-collapse#nav_collapse(is-nav)
b-navbar-nav
b-nav-item(v-if='!logged' to='/login' v-b-tooltip :title='$t("Login")') <v-icon color='lightgreen' name='lock' />
span.d-md-none {{$t('User')}}
b-nav-item(to='/new_event' v-b-tooltip :title='$t("Add Event")' ) <v-icon color='lightgreen' name='plus'/>
span.d-md-none {{$t('Add Event')}}
b-nav-item(v-if='logged' to='/settings' v-b-tooltip :title='$t("Settings")') <v-icon color='orange' name='cog'/>
span.d-md-none {{$t('Settings')}}
b-nav-item(v-if='user.is_admin' to='/admin' v-b-tooltip :title='$t("Admin")') <v-icon color='lightblue' name='tools'/>
span.d-md-none {{$t('Admin')}}
b-nav-item(to='/export' v-b-tooltip :title='$t("Export")') <v-icon name='file-export' color='yellow'/>
span.d-md-none {{$t('Export')}}
b-nav-item(v-if='logged' @click='logout' v-b-tooltip :title='$t("Logout")') <v-icon color='red' name='sign-out-alt'/>
span.d-md-none {{$t('Logout')}}
b-navbar-nav.ml-auto
b-nav-item(to='/about')
span {{$t('Info')}} <v-icon color='#ff9fc4' name='question-circle'/>
</template>
<script>
import {mapState, mapActions} from 'vuex'
export default {
name: 'Nav',
computed: {
...mapState(['logged', 'user','filters']),
filters_tags: {
set (value) {
this.setSearchTags(value)
},
get () {
return this.filters.tags
}
},
filters_places: {
set (value) {
this.setSearchPlaces(value)
},
get () {
return this.filters.places
}
},
},
methods: mapActions(['logout']),
}
</script>

View File

@@ -1,56 +0,0 @@
<template lang='pug'>
b-modal(@shown="$refs.password.focus()" :title='$t("Password reset")' hide-footer
@hidden='$router.replace("/")' :visible='true' ref='modal')
div(v-loading='loading')
p(v-html="$t('recover_explanation')")
span(v-if='user.email') {{user.email}}
el-input.mb-2(v-model='password' type='password' :placeholder='$t("New password")' ref='password')
v-icon(name='lock' slot='prepend')
el-button.mr-1(plain type="success" @click='change') {{$t('Change')}}
</template>
<script>
import api from '@/api'
import { mapActions } from 'vuex'
import { Message } from 'element-ui'
export default {
name: 'Recover',
data () {
return {
password: '',
loading: true,
user: {}
}
},
async mounted () {
try {
this.user = await api.checkRecoverCode(this.$route.params.recover_code)
console.log(this.user)
} catch (e) {
this.$refs.modal.hide()
Message({message: this.$t('error_recover_code'), type: 'error'})
}
this.loading = false
},
methods: {
async change () {
if (!this.password) return
this.loading = true
try {
await api.recoverPassword(this.$route.params.recover_code, this.password)
} catch (e) {
this.loading = false
Message({ message: this.$t('password_not_changed'), type: 'error'})
return
}
this.loading = false
// this.$refs.modal.hide()
Message({ message: this.$t('password_changed'), type: 'success'})
this.$router.replace('/login')
}
}
}
</script>

View File

@@ -1,60 +0,0 @@
<template lang='pug'>
b-modal(hide-footer @hidden='$router.replace("/")' ref='modal'
:title="$t('Register')" :visible='true' @shown='$refs.email.focus()')
el-form
p(v-html="$t('register_explanation')")
el-input.mb-2(ref='email' v-model='user.email' type='email'
:placeholder='$t("Email")' autocomplete='email')
span(slot='prepend') @
el-input.mb-2(v-model='user.password' type="password" placeholder="Password")
v-icon(name='lock' slot='prepend')
el-input.mb-2(v-model='user.description' type="textarea" rows='3' :placeholder="$t('Description')")
v-icon(name='envelope-open-text')
el-button.float-right(plain type="success" icon='el-icon-arrow-right' @click='register') {{$t('Send')}}
</template>
<script>
import api from '@/api'
import { mapActions } from 'vuex';
import { Message } from 'element-ui'
export default {
name: 'Register',
data () {
return {
error: {},
user: { }
}
},
methods: {
...mapActions(['login']),
async register () {
try {
const user = await api.register(this.user)
if (!user.is_admin) {
this.$refs.modal.hide()
Message({
message: this.$t('registration_complete'),
type: 'success'
})
} else {
Message({
message: this.$t('admin_registration_complete'),
type: 'success'
})
}
} catch (e) {
Message({
message: e,
type: 'error'
})
console.error(e)
}
}
}
}
</script>

View File

@@ -1,39 +0,0 @@
<template lang="pug">
div
el-select.mr-1(v-model='filters_places' multiple filterable collapse-tags
default-first-option :placeholder='$t("Where")')
el-option(v-for='place in places' :value='place.name'
:label='place.name' :key='place.id')
el-select(v-model='filters_tags' multiple filterable collapse-tags
default-first-option :placeholder='$t("Tags")')
el-option(v-for='tag in tags' :key='tag.tag'
:label='tag.tag' :value='tag.tag')
</template>
<script>
import {mapState, mapActions} from 'vuex'
export default {
name :'Search',
methods: mapActions(['setSearchPlaces', 'setSearchTags']),
computed: {
...mapState(['tags', 'places', 'filters']),
filters_tags: {
set (value) {
this.setSearchTags(value)
},
get () {
return this.filters.tags
}
},
filters_places: {
set (value) {
this.setSearchPlaces(value)
},
get () {
return this.filters.places
}
},
}
}
</script>

View File

@@ -1,60 +0,0 @@
<template lang="pug">
b-modal(:title="$t('Settings')" hide-footer @hidden='$router.replace("/")' :visible='true')
h5 {{user.name}}
el-input(v-model="mastodon_instance" @enter.native='associate')
span(slot='prepend') {{$t('Mastodon instance')}}
el-button(v-if='!user.mastodon_auth' slot='append' @click='associate' type='success') {{$t('Associate')}}
el-button(v-else slot='append' @click='deassociate' variant='success') {{$t('De-associate')}}
el-input.mt-2(v-model='password' type='password')
span(slot='prepend') {{$t('Change password')}}
el-button(slot='append' @click='change' type='success') {{$t('Change')}}
</template>
<script>
import { mapState, mapActions } from 'vuex'
import api from '@/api'
export default {
props: ['code'],
data () {
return {
mastodon_instance: '',
password: '',
user: {}
}
},
computed: mapState(['oauth', 'user']),
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.mastodon_auth.instance
},
methods: {
async change () {
if (!this.password) return
const user = this.user
user.password = this.password
try {
await api.updateUser(user)
} catch (e) {
console.log(e)
}
},
async deassociate () {
const user = this.user
user.mastodon_auth = ''
this.mastodon_instance = ''
await api.updateUser(user)
},
async associate () {
if (!this.mastodon_instance) return
const url = await api.getAuthURL({instance: this.mastodon_instance})
setTimeout( () => window.location.href=url, 100);
}
}
}
</script>

View File

@@ -1,17 +0,0 @@
// <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>

View File

@@ -1,9 +0,0 @@
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'
export default { Home, eventDetail, newEvent, Settings, Login, Register, Event }

View File

@@ -1,215 +0,0 @@
<template lang="pug">
b-modal(ref='modal' @hidden='$router.replace("/")' size='lg' :visible='true'
:title="edit?$t('Edit event'):$t('New event')" hide-footer)
el-form
el-tabs.mb-2(v-model='activeTab' v-loading='sending')
//- NOT LOGGED EVENT
el-tab-pane(v-if='!logged')
span(slot='label') {{$t('anon_newevent')}} <v-icon name='user-secret'/>
p(v-html="$t('anon_newevent_explanation')")
el-button.float-right(@click='next' :disabled='!couldProceed') {{$t('Next')}}
//- WHERE
el-tab-pane
span(slot='label') {{$t('Where')}} <v-icon name='map-marker-alt'/>
div {{$t('where_explanation')}}
el-select.mb-3(v-model='event.place.name' @change='placeChoosed' filterable allow-create default-first-option)
el-option(v-for='place in places_name' :label='place' :value='place' :key='place.id')
div {{$t("Address")}}
el-input.mb-3(ref='address' v-model='event.place.address' @keydown.native.enter='next')
el-button.float-right(@click='next' :disabled='!couldProceed') {{$t('Next')}}
//- WHEN
el-tab-pane
span(slot='label') {{$t('When')}} <v-icon name='clock'/>
span {{event.multidate ? $t('dates_explanation') : $t('date_explanation')}}
el-switch.float-right(v-model='event.multidate' :active-text="$t('multidate_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')
div {{$t('time_start_explanation')}}
el-time-select.mb-3(ref='time_start'
v-model="time.start"
:picker-options="{ start: '00:00', step: '00:30', end: '24:00'}")
div {{$t('time_end_explanation')}}
el-time-select(v-model='time.end'
:picker-options="{start: '00:00', step: '00:30', end: '24:00'}")
el-button.float-right(@click='next' :disabled='!couldProceed') {{$t('Next')}}
//- WHAT
el-tab-pane
span(slot='label') {{$t('What')}} <v-icon name='file-alt'/>
span {{$t('what_explanation')}}
el-input.mb-3(v-model='event.title' ref='title')
span {{$t('description_explanation')}}
el-input.mb-3(v-model='event.description' type='textarea' :rows='9')
span {{$t('tag_explanation')}}
br
el-select(v-model='event.tags' multiple filterable allow-create
default-first-option placeholder='Tag')
el-option(v-for='tag in tags' :key='tag.tag'
:label='tag' :value='tag')
el-button.float-right(@click.native='next' :disabled='!couldProceed') {{$t('Next')}}
el-tab-pane
span(slot='label') {{$t('Media')}} <v-icon name='image'/>
span {{$t('media_explanation')}}
b-form-file.mb-2(v-model='event.image', :placeholder='$t("Poster")' accept='image/*')
el-button.float-right(@click='done') {{edit?$t('Edit'):$t('Send')}}
</template>
<script>
import api from '@/api'
import { mapActions, mapState } from 'vuex'
import moment from 'dayjs'
import Calendar from './Calendar'
import { Message } from 'element-ui'
export default {
components: { Calendar },
data() {
return {
event: {
place: { name: '', address: '' },
title: '', description: '', tags: [],
multidate: false,
},
visible: true,
id: null,
activeTab: "0",
date: null,
time: { start: '20:00', end: null },
edit: false,
sending: false,
}
},
name: 'newEvent',
watch: {
'time.start' (value) {
let [h, m] = value.split(':')
this.time.end = (Number(h)+1) + ':' + m
}
},
async mounted () {
if (this.$route.params.id) {
this.id = this.$route.params.id
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.replace(/(<([^>]+)>)/ig, '')
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,
user: state => state.user,
logged: state => state.logged
}),
couldProceed () {
const t = this.logged ? -1 : 0
switch(Number(this.activeTab)) {
case 0+t:
return true
case 1+t:
return this.event.place.name.length>0 &&
this.event.place.address.length>0
case 2+t:
if (this.date && this.time.start) return true
break
case 3+t:
return this.event.title.length>0
break
case 4+t:
return true
break
}
}
},
methods: {
...mapActions(['addEvent', 'updateEvent', 'updateMeta']),
next () {
this.activeTab = String(Number(this.activeTab)+1)
if (this.activeTab === "2") {
this.$refs.title.focus()
}
},
prev () {
this.activeTab = String(Number(this.activeTab-1))
},
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)
.set('hour', start_hour).set('minute', start_minute)
end_datetime = moment(this.date.end)
.set('hour', end_hour).set('minute', end_minute)
} else {
console.log(this.date)
start_datetime = moment(this.date).set('hour', start_hour).set('minute', start_minute)
end_datetime = moment(this.date).set('hour', end_hour).set('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))
this.sending = true
try {
if (this.edit) {
await this.updateEvent(formData)
} else {
await this.addEvent(formData)
}
this.updateMeta()
this.sending = false
this.$refs.modal.hide()
Message({ type: 'success', message: this.logged ? this.$t('new_event_added') : this.$t('new_anon_event_added')})
} catch (e) {
this.sending = false
console.error(e)
}
}
}
}
</script>

View File

@@ -1,15 +0,0 @@
import moment from 'dayjs'
import 'dayjs/locale/it'
moment.locale('it')
export default {
datetime (value) {
return moment(value).format('ddd, D MMMM HH:mm')
},
short_datetime (value) {
return moment(value).format('D/MM HH:mm')
},
hour (value) {
return moment(value).format('HH:mm')
}
}

View File

@@ -1,31 +0,0 @@
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: `To follow updates from pc and mobile with, we reccomend the use of RSS.<br/>
Thanks to RSS you can use any application able to receive updates from the websites you like.
Using RSS you can to follow in a quick way all the websites you like with no need to create an account<br/>
If you have Android, you can use <a href="https://play.google.com/store/apps/details?id=net.frju.flym">Flym</a> or Feeder<br />
If you have iOS you can use <a href="https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8">Feed4U</a><br />
For computers you can use the Browser plugin Feedbro,
<a href="https://addons.mozilla.org/en-GB/firefox/addon/feedbroreader/">[ for Firefox ] </a>
and <a href="https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa">[ for Chrome ]</a> </br>
Feedbro is compatible with every computer using Firefox or Chrome.</p><br/>
Using the link above you will be updated on the following events:`,
SignIn: 'Sign In',
registration_email: `Hi, your registration will be confirmed soon.`,
register_explanation: `..`
}
export default en

View File

@@ -1,109 +0,0 @@
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: 'Orario di inizio',
time_end_explanation: '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. Per questo puoi rimanere aggiornata sugli eventi che vuoi, come meglio credi, senza necessariamente passare da questo sito.`,
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>
<li>Se hai Android, ti consigliamo <a href="https://play.google.com/store/apps/details?id=net.frju.flym">Flym</a> o Feeder</li>
<li>Per iPhone/iPad puoi usare <a href="https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8">Feed4U</a></li>
<li>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.</li>
<br/>
Aggiungendo questo link al tuo lettore di feed, rimarrai aggiornata.`,
export_email_explanation: `Puoi ricevere via mail gli eventi che ti interessano.`,
export_list_explanation: `Se hai un sito web e vuoi mostrare una lista di eventi, puoi usare il seguente codice (ehm, questa funzionalita' la stiamo ancora sviluppando)`,
export_calendar_explanation: `Se hai un sito web e vuoi mostrare un calendario di eventi, puoi usare il seguente codice (ehm, questa funzionalita' la stiamo ancora sviluppando)`,
export_ical_explanation: `I computer e gli smartphone sono comunemente attrezzati con un'applicazione per gestire un calendario. A questi programmi solitamente è possibile far importare un calendario remoto.`,
Poster: 'Locandina',
Settings: 'Impostazioni',
'Change password': 'Cambia password',
Change: 'Cambia',
Search: 'Cerca',
Send: 'Invia',
Register: 'Registrati',
Logout: 'Esci',
Login: 'Entra',
SignIn: 'Registrati',
Cancel: 'Annulla',
Copy: 'Copia',
Next: 'Continua',
Prev: 'Indietro',
Username: 'Utente',
Description: 'Descrizione',
Deactivate: 'Disattiva',
Activate: 'Attiva',
'Remove Admin': 'Rimuovi Admin',
'Remove notification': 'Rimuovi notifiche',
Yes: 'Sì',
Users: 'Utenti',
Places: 'Luoghi',
Tags: 'Etichette',
Name: 'Nome',
Preview: 'Visualizza',
Save: 'Salva',
Address: 'Indirizzo',
Remove: 'Elimina',
Password: 'Password',
Email: 'Email',
User: 'Utente',
Unconfirm: 'Disabilita',
Confirm: 'Conferma',
Events: 'Eventi',
Color: 'Colore',
Edit: 'Modifica',
Admin: 'Amministra',
Today: 'Oggi',
Export: 'Esporta',
'Forgot password?': 'Hai dimenticato la password?',
'Mastodon instance': 'Istanza Mastodon',
'admin_mastodon_explanation': 'Puoi associare un account mastodon a questa istanza di gancio, ogni evento verrà pubblicato lì',
Associate: 'Associa',
admin_tag_explanation: `Seleziona un'etichetta per cambiarne il colore`,
event_confirmed: 'Evento confermato!',
notify_on_insert: `Notifica all'inserimento`,
'event_confirm_explanation': 'Puoi approvare gli eventi inseriti da utenti non registrati',
'admin_place_explanation': 'Puoi modificare i luoghi inseriti',
'Edit event': 'Modifica evento',
'New event': 'Nuovo evento',
anon_newevent: 'Evento senza autore',
anon_newevent_explanation: `Puoi inserire un evento senza rigistrarti o fare il login, ma in questo caso dovrai aspettare che qualcuno lo legga confermando che si tratta di un evento adatto a questo spazio, delegando questa scelta.<br/><br/> Puoi fare il <a href='/login'>login</a> o <a href='/registrarti'>registrarti</a>, altrimenti vai avanti e riceverai una risposta il prima possibile!`,
'Insert your address': 'Inserisci il tuo indirizzo',
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, usatelo solo per 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.`,
'Not registered?': `Non sei registrata?`,
new_event_added: `Evento aggiunto.`,
new_anon_event_added: `Evento inserito, verrà confermato quanto prima.`,
login_explanation: `Entrando puoi pubblicare nuovi eventi.`,
recover_explanation: `Inserisci una nuova password:`,
'New password': 'Nuova password',
'Password reset': 'Password reset',
password_changed: 'Password cambiata con successo',
password_not_changed: 'Errore nella modifica della password!',
'Insert your email': 'Inserisci la tua email',
error_recover_code: `Ops, c'è stato un problema, hai richiesto più reset password?`,
email_notification_activated: 'Notifiche via mail attivate. Per disattivarle puoi usare il link in fondo ad ogni email che ricevi.'
}
export default it

View File

@@ -1,112 +0,0 @@
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import BootstrapVue from 'bootstrap-vue'
import VCalendar from 'v-calendar'
import 'vue-awesome/icons/lock'
import 'vue-awesome/icons/user'
import 'vue-awesome/icons/plus'
import 'vue-awesome/icons/cog'
import 'vue-awesome/icons/tools'
import 'vue-awesome/icons/file-export'
import 'vue-awesome/icons/sign-out-alt'
import 'vue-awesome/icons/clock'
import 'vue-awesome/icons/map-marker-alt'
import 'vue-awesome/icons/file-alt'
import 'vue-awesome/icons/image'
import 'vue-awesome/icons/tag'
import 'vue-awesome/icons/users'
import 'vue-awesome/icons/calendar'
import 'vue-awesome/icons/edit'
import 'vue-awesome/icons/envelope-open-text'
import 'vue-awesome/icons/user-secret'
import 'vue-awesome/icons/question-circle'
import Icon from 'vue-awesome/components/Icon'
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 { Button, Select, Tag, Option, Table, FormItem, Card,
Form, Tabs, TabPane, Switch, Input, Loading, TimeSelect,
TableColumn, ColorPicker, Pagination, Popover } from 'element-ui'
import ElementLocale from 'element-ui/lib/locale'
import MagicGrid from 'vue-magic-grid'
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 './assets/main.css'
import itLocale from '@/locale/it'
import enLocale from '@/locale/en'
Vue.use(Button)
Vue.use(Popover)
Vue.use(Card)
Vue.use(Select)
Vue.use(Tag)
Vue.use(Input)
Vue.use(Tabs)
Vue.use(TabPane)
Vue.use(Option)
Vue.use(Switch)
Vue.use(ColorPicker)
Vue.use(Table)
Vue.use(TableColumn)
Vue.use(Pagination)
Vue.use(FormItem)
Vue.use(Form)
Vue.use(TimeSelect)
Vue.use(Loading.directive)
Vue.use(MagicGrid)
// 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('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.locale('en', enLocale)
Vue.config.devtools = true
Vue.config.silent = false
ElementLocale.i18n((key, value) => i18n.t(key, value))
new Vue({
i18n,
router,
store,
render: h => h(App)
}).$mount('#app')

View File

@@ -1,70 +0,0 @@
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'
import About from './components/About'
import DelNotification from './components/DelNotification'
import Recover from './components/Recover'
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',
components: { modal: Export }
},
{
path: '/admin/oauth',
components: { modal: Admin }
},
{
path: '/about',
components: { modal: About }
},
{
path: '/del_notification/:code',
components: { modal: DelNotification }
},
{
path: '/recover/:recover_code',
components: { modal: Recover }
}
]
})

View File

@@ -1,147 +0,0 @@
import Vue from 'vue'
import Vuex from 'vuex'
import VuexPersistence from 'vuex-persist'
import { intersection } from 'lodash'
import api from './api'
import moment from 'dayjs'
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,
filteredEvents: state => {
const events = state.events.map(e => {
const end_datetime = e.end_datetime || moment(e.start_datetime).add('3', 'hour')
const past = (moment().diff(end_datetime, 'minutes') > 0)
e.past = past
return e
})
if (!state.filters.tags.length && !state.filters.places.length) {
return events
}
return events.filter(e => {
if (state.filters.tags.length) {
const m = intersection(e.tags.map(t => t.tag), state.filters.tags)
if (m.length > 0) return true
}
if (state.filters.places.length) {
if (state.filters.places.find(p => p === e.place.name)) {
return true
}
}
return 0
})
}
},
state: {
logged: false,
user: {},
token: '',
events: [],
tags: [],
places: [],
//
filters: {
tags: [],
places: []
}
},
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)
} else {
state.filters.tags = state.filters.tags.filter(t => t !== 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)
if (this.state.logged) {
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)
}
}
})

View File

@@ -1,21 +0,0 @@
const webpack = require('webpack')
process.env.VUE_APP_API = process.env.NODE_ENV === 'production' ? process.env.BASE_URL || 'http://localhost:9000' : 'http://localhost:9000'
process.env.VUE_APP_TITLE = process.env.TITLE || 'Gancio'
process.env.VUE_APP_DESCRIPTION = process.env.DESCRIPTION || 'Event manager for radical movements'
module.exports = {
publicPath: process.env.BASE_URL,
configureWebpack: {
plugins: [
new webpack.NormalModuleReplacementPlugin(/element-ui[/\\]lib[/\\]locale[/\\]lang[/\\]zh-CN/, 'element-ui/lib/locale/lang/en')
]
},
devServer: {
disableHostCheck: true
},
transpileDependencies: [
/\bvue-awesome\b/,
'vuex-persist'
]
}

File diff suppressed because it is too large Load Diff

BIN
db.sqlite

Binary file not shown.

View File

@@ -1,25 +0,0 @@
version: '3.5'
services:
db:
image: 'postgres:latest'
environment:
POSTGRES_PASSWORD: docker
POSTGRES_USER: docker
POSTGRES_DB: gancio
volumes:
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
- ./postgres:/var/lib/postgresql/data
app:
env_file: .env.production
build: .
ports:
- '12300:12300'
volumes:
- ./uploads:/usr/src/app/uploads
links:
- db

596
init.sql
View File

@@ -1,596 +0,0 @@
--
-- PostgreSQL database dump
--
-- Dumped from database version 11.2 (Debian 11.2-1.pgdg90+1)
-- Dumped by pg_dump version 11.2 (Debian 11.2-1.pgdg90+1)
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET client_min_messages = warning;
SET row_security = off;
--
-- Name: enum_EventNotifications_status; Type: TYPE; Schema: public; Owner: docker
--
CREATE TYPE public."enum_EventNotifications_status" AS ENUM (
'new',
'sent',
'error'
);
ALTER TYPE public."enum_EventNotifications_status" OWNER TO docker;
--
-- Name: enum_notifications_type; Type: TYPE; Schema: public; Owner: docker
--
CREATE TYPE public.enum_notifications_type AS ENUM (
'mail',
'admin_email',
'mastodon'
);
ALTER TYPE public.enum_notifications_type OWNER TO docker;
SET default_tablespace = '';
SET default_with_oids = false;
--
-- Name: EventNotifications; Type: TABLE; Schema: public; Owner: docker
--
CREATE TABLE public."EventNotifications" (
status public."enum_EventNotifications_status" DEFAULT 'new'::public."enum_EventNotifications_status",
"createdAt" timestamp with time zone NOT NULL,
"updatedAt" timestamp with time zone NOT NULL,
"eventId" integer NOT NULL,
"notificationId" integer NOT NULL
);
ALTER TABLE public."EventNotifications" OWNER TO docker;
--
-- Name: comments; Type: TABLE; Schema: public; Owner: docker
--
CREATE TABLE public.comments (
id integer NOT NULL,
activitypub_id integer,
author character varying(255),
text character varying(255),
"createdAt" timestamp with time zone NOT NULL,
"updatedAt" timestamp with time zone NOT NULL,
"eventId" integer
);
ALTER TABLE public.comments OWNER TO docker;
--
-- Name: comments_id_seq; Type: SEQUENCE; Schema: public; Owner: docker
--
CREATE SEQUENCE public.comments_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE public.comments_id_seq OWNER TO docker;
--
-- Name: comments_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: docker
--
ALTER SEQUENCE public.comments_id_seq OWNED BY public.comments.id;
--
-- Name: events; Type: TABLE; Schema: public; Owner: docker
--
CREATE TABLE public.events (
id integer NOT NULL,
title character varying(255),
description text,
multidate boolean,
start_datetime timestamp with time zone,
end_datetime timestamp with time zone,
image_path character varying(255),
activitypub_id integer,
is_visible boolean,
"createdAt" timestamp with time zone NOT NULL,
"updatedAt" timestamp with time zone NOT NULL,
"userId" integer,
"placeId" integer
);
ALTER TABLE public.events OWNER TO docker;
--
-- Name: events_id_seq; Type: SEQUENCE; Schema: public; Owner: docker
--
CREATE SEQUENCE public.events_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE public.events_id_seq OWNER TO docker;
--
-- Name: events_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: docker
--
ALTER SEQUENCE public.events_id_seq OWNED BY public.events.id;
--
-- Name: notifications; Type: TABLE; Schema: public; Owner: docker
--
CREATE TABLE public.notifications (
id integer NOT NULL,
filters json,
email character varying(255),
remove_code character varying(255),
type public.enum_notifications_type,
"createdAt" timestamp with time zone NOT NULL,
"updatedAt" timestamp with time zone NOT NULL
);
ALTER TABLE public.notifications OWNER TO docker;
--
-- Name: notifications_id_seq; Type: SEQUENCE; Schema: public; Owner: docker
--
CREATE SEQUENCE public.notifications_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE public.notifications_id_seq OWNER TO docker;
--
-- Name: notifications_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: docker
--
ALTER SEQUENCE public.notifications_id_seq OWNED BY public.notifications.id;
--
-- Name: places; Type: TABLE; Schema: public; Owner: docker
--
CREATE TABLE public.places (
id integer NOT NULL,
name character varying(255),
address character varying(255),
"createdAt" timestamp with time zone NOT NULL,
"updatedAt" timestamp with time zone NOT NULL
);
ALTER TABLE public.places OWNER TO docker;
--
-- Name: places_id_seq; Type: SEQUENCE; Schema: public; Owner: docker
--
CREATE SEQUENCE public.places_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE public.places_id_seq OWNER TO docker;
--
-- Name: places_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: docker
--
ALTER SEQUENCE public.places_id_seq OWNED BY public.places.id;
--
-- Name: settings; Type: TABLE; Schema: public; Owner: docker
--
CREATE TABLE public.settings (
key character varying(255) NOT NULL,
value json,
"createdAt" timestamp with time zone NOT NULL,
"updatedAt" timestamp with time zone NOT NULL
);
ALTER TABLE public.settings OWNER TO docker;
--
-- Name: tagEvent; Type: TABLE; Schema: public; Owner: docker
--
CREATE TABLE public."tagEvent" (
"createdAt" timestamp with time zone NOT NULL,
"updatedAt" timestamp with time zone NOT NULL,
"eventId" integer NOT NULL,
"tagTag" character varying(255) NOT NULL
);
ALTER TABLE public."tagEvent" OWNER TO docker;
--
-- Name: tags; Type: TABLE; Schema: public; Owner: docker
--
CREATE TABLE public.tags (
tag character varying(255) NOT NULL,
color character varying(255),
"createdAt" timestamp with time zone NOT NULL,
"updatedAt" timestamp with time zone NOT NULL
);
ALTER TABLE public.tags OWNER TO docker;
--
-- Name: users; Type: TABLE; Schema: public; Owner: docker
--
CREATE TABLE public.users (
id integer NOT NULL,
email character varying(255) NOT NULL,
description text,
password character varying(255),
recover_code character varying(255),
is_admin boolean,
is_active boolean,
mastodon_auth json,
"createdAt" timestamp with time zone NOT NULL,
"updatedAt" timestamp with time zone NOT NULL
);
ALTER TABLE public.users OWNER TO docker;
--
-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: docker
--
CREATE SEQUENCE public.users_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE public.users_id_seq OWNER TO docker;
--
-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: docker
--
ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id;
--
-- Name: comments id; Type: DEFAULT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public.comments ALTER COLUMN id SET DEFAULT nextval('public.comments_id_seq'::regclass);
--
-- Name: events id; Type: DEFAULT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public.events ALTER COLUMN id SET DEFAULT nextval('public.events_id_seq'::regclass);
--
-- Name: notifications id; Type: DEFAULT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public.notifications ALTER COLUMN id SET DEFAULT nextval('public.notifications_id_seq'::regclass);
--
-- Name: places id; Type: DEFAULT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public.places ALTER COLUMN id SET DEFAULT nextval('public.places_id_seq'::regclass);
--
-- Name: users id; Type: DEFAULT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass);
--
-- Data for Name: EventNotifications; Type: TABLE DATA; Schema: public; Owner: docker
--
COPY public."EventNotifications" (status, "createdAt", "updatedAt", "eventId", "notificationId") FROM stdin;
\.
--
-- Data for Name: comments; Type: TABLE DATA; Schema: public; Owner: docker
--
COPY public.comments (id, activitypub_id, author, text, "createdAt", "updatedAt", "eventId") FROM stdin;
\.
--
-- Data for Name: events; Type: TABLE DATA; Schema: public; Owner: docker
--
COPY public.events (id, title, description, multidate, start_datetime, end_datetime, image_path, activitypub_id, is_visible, "createdAt", "updatedAt", "userId", "placeId") FROM stdin;
\.
--
-- Data for Name: notifications; Type: TABLE DATA; Schema: public; Owner: docker
--
COPY public.notifications (id, filters, email, remove_code, type, "createdAt", "updatedAt") FROM stdin;
\.
--
-- Data for Name: places; Type: TABLE DATA; Schema: public; Owner: docker
--
COPY public.places (id, name, address, "createdAt", "updatedAt") FROM stdin;
\.
--
-- Data for Name: settings; Type: TABLE DATA; Schema: public; Owner: docker
--
COPY public.settings (key, value, "createdAt", "updatedAt") FROM stdin;
\.
--
-- Data for Name: tagEvent; Type: TABLE DATA; Schema: public; Owner: docker
--
COPY public."tagEvent" ("createdAt", "updatedAt", "eventId", "tagTag") FROM stdin;
\.
--
-- Data for Name: tags; Type: TABLE DATA; Schema: public; Owner: docker
--
COPY public.tags (tag, color, "createdAt", "updatedAt") FROM stdin;
\.
--
-- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: docker
--
COPY public.users (id, email, description, password, recover_code, is_admin, is_active, mastodon_auth, "createdAt", "updatedAt") FROM stdin;
\.
--
-- Name: comments_id_seq; Type: SEQUENCE SET; Schema: public; Owner: docker
--
SELECT pg_catalog.setval('public.comments_id_seq', 1, false);
--
-- Name: events_id_seq; Type: SEQUENCE SET; Schema: public; Owner: docker
--
SELECT pg_catalog.setval('public.events_id_seq', 1, false);
--
-- Name: notifications_id_seq; Type: SEQUENCE SET; Schema: public; Owner: docker
--
SELECT pg_catalog.setval('public.notifications_id_seq', 1, false);
--
-- Name: places_id_seq; Type: SEQUENCE SET; Schema: public; Owner: docker
--
SELECT pg_catalog.setval('public.places_id_seq', 1, false);
--
-- Name: users_id_seq; Type: SEQUENCE SET; Schema: public; Owner: docker
--
SELECT pg_catalog.setval('public.users_id_seq', 1, false);
--
-- Name: EventNotifications EventNotifications_pkey; Type: CONSTRAINT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public."EventNotifications"
ADD CONSTRAINT "EventNotifications_pkey" PRIMARY KEY ("eventId", "notificationId");
--
-- Name: comments comments_pkey; Type: CONSTRAINT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public.comments
ADD CONSTRAINT comments_pkey PRIMARY KEY (id);
--
-- Name: events events_pkey; Type: CONSTRAINT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public.events
ADD CONSTRAINT events_pkey PRIMARY KEY (id);
--
-- Name: notifications notifications_pkey; Type: CONSTRAINT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public.notifications
ADD CONSTRAINT notifications_pkey PRIMARY KEY (id);
--
-- Name: places places_name_key; Type: CONSTRAINT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public.places
ADD CONSTRAINT places_name_key UNIQUE (name);
--
-- Name: places places_pkey; Type: CONSTRAINT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public.places
ADD CONSTRAINT places_pkey PRIMARY KEY (id);
--
-- Name: settings settings_pkey; Type: CONSTRAINT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public.settings
ADD CONSTRAINT settings_pkey PRIMARY KEY (key);
--
-- Name: tagEvent tagEvent_pkey; Type: CONSTRAINT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public."tagEvent"
ADD CONSTRAINT "tagEvent_pkey" PRIMARY KEY ("eventId", "tagTag");
--
-- Name: tags tags_pkey; Type: CONSTRAINT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public.tags
ADD CONSTRAINT tags_pkey PRIMARY KEY (tag);
--
-- Name: users users_email_key; Type: CONSTRAINT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public.users
ADD CONSTRAINT users_email_key UNIQUE (email);
--
-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public.users
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
--
-- Name: EventNotifications EventNotifications_eventId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public."EventNotifications"
ADD CONSTRAINT "EventNotifications_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES public.events(id) ON UPDATE CASCADE ON DELETE CASCADE;
--
-- Name: EventNotifications EventNotifications_notificationId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public."EventNotifications"
ADD CONSTRAINT "EventNotifications_notificationId_fkey" FOREIGN KEY ("notificationId") REFERENCES public.notifications(id) ON UPDATE CASCADE ON DELETE CASCADE;
--
-- Name: comments comments_eventId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public.comments
ADD CONSTRAINT "comments_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES public.events(id) ON UPDATE CASCADE ON DELETE SET NULL;
--
-- Name: events events_placeId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public.events
ADD CONSTRAINT "events_placeId_fkey" FOREIGN KEY ("placeId") REFERENCES public.places(id) ON UPDATE CASCADE ON DELETE SET NULL;
--
-- Name: events events_userId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public.events
ADD CONSTRAINT "events_userId_fkey" FOREIGN KEY ("userId") REFERENCES public.users(id) ON UPDATE CASCADE ON DELETE SET NULL;
--
-- Name: tagEvent tagEvent_eventId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public."tagEvent"
ADD CONSTRAINT "tagEvent_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES public.events(id) ON UPDATE CASCADE ON DELETE CASCADE;
--
-- Name: tagEvent tagEvent_tagTag_fkey; Type: FK CONSTRAINT; Schema: public; Owner: docker
--
ALTER TABLE ONLY public."tagEvent"
ADD CONSTRAINT "tagEvent_tagTag_fkey" FOREIGN KEY ("tagTag") REFERENCES public.tags(tag) ON UPDATE CASCADE ON DELETE CASCADE;
--
-- PostgreSQL database dump complete
--

View File

@@ -1,6 +0,0 @@
{
"registration_email": "Ciao, la tua registrazione sarà confermata nei prossimi giorni. Riceverai una conferma non temere.",
"confirm_email": "confirm_email",
"recover_email": "recover_email",
"press here": "press here"
}

View File

@@ -1,6 +0,0 @@
{
"registration_email": "registration_email",
"confirm_email": "confirm_email",
"recover_email": "recover_email",
"press here": "press here"
}

View File

@@ -1,6 +0,0 @@
{
"registration_email": "Ciao, la tua registrazione sarà confermata nei prossimi giorni. Riceverai una conferma non temere.",
"confirm_email": "Abbiamo attivato il tuo utente su gancio. Ora puoi aggiungere eventi.",
"recover_email": "Qualcuno ha richiesto un reset della password su gancio, se non sei stata tu, ignora pure questa email altrimenti visita questo link:",
"press here": "Premi qui"
}

View File

@@ -1,6 +0,0 @@
{
"registration_email": "registration_email",
"confirm_email": "confirm_email",
"recover_email": "recover_email",
"press here": "press here"
}

View File

@@ -1,35 +0,0 @@
{
"name": "gancio",
"main": "server.js",
"scripts": {
"serve": "NODE_ENV=production nodemon app/server.js",
"dev": "NODE_ENV=development nodemon app/server.js"
},
"dependencies": {
"bcrypt": "^3.0.2",
"body-parser": "^1.15.0",
"cors": "^2.8.4",
"email-templates": "^5.0.2",
"express": "^4.13.4",
"ics": "^2.13.1",
"jsonwebtoken": "^5.7.0",
"mastodon-api": "^1.3.0",
"mongoose": "^5.2.17",
"morgan": "^1.7.0",
"multer": "^1.4.1",
"mysql2": "^1.6.4",
"pg": "^7.8.1",
"pug": "^2.0.3",
"sequelize": "^4.41.0",
"sharp": "^0.21.3",
"sqlite3": "^4.0.3"
},
"devDependencies": {
"eslint": "^5.8.0",
"eslint-config-standard": "^12.0.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-node": "^8.0.0",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-standard": "^4.0.0"
}
}

View File

@@ -1,12 +0,0 @@
{
"apps": [
{
"name": "GANCIO",
"script": "./app/server.js"
},
{
"name": "WORKER",
"script": "./app/cron.js"
}
]
}

View File