front end, config

This commit is contained in:
lesion
2019-02-26 01:17:52 +01:00
parent 887157f2a9
commit bae404f422
58 changed files with 12862 additions and 157 deletions
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
"extends": "standard"
};
+1 -3
View File
@@ -31,12 +31,10 @@ api.route('/user/event')
api.route('/user/event/:id')
.delete(isAuth, userController.delEvent)
api.get('/event/meta', eventController.getMeta)
api.route('/event/:event_id')
.get(eventController.get)
api.route('/event/meta')
.get(eventController.getMeta)
api.get('/export/feed', exportController.feed)
api.get('/export/ics', exportController.ics)
+1 -1
View File
@@ -10,7 +10,7 @@ const Auth = {
jwt.verify(token, config.secret, async (err, decoded) => {
if (err) return res.status(403).send({ message: 'Failed to authenticate token ' + err })
console.log('DECODED TOKEN', decoded)
req.user = await User.findOne({ where: {email: decoded.email}})
req.user = await User.findOne({ where: { email: decoded.email } })
next()
})
},
+2 -1
View File
@@ -1,2 +1,3 @@
const env = process.env.NODE_ENV
module.exports = require('../config/config.' + env + '.js')
const conf = require('../config/config.' + env + '.json')
module.exports = conf
+16 -18
View File
@@ -9,34 +9,33 @@ const moment = require('moment')
moment.locale('it')
const botController = {
bots: [],
async initialize() {
async initialize () {
console.log('initialize bots')
const botUsers = await User.findAll({where: { mastodon_auth: { [Op.ne]: null }}})
const botUsers = await User.findAll({ where: { mastodon_auth: { [Op.ne]: null } } })
console.log(botUsers)
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.instance}/api/v1/` })
console.log(bot)
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}
return { email: user.email, bot }
})
console.log(botController.bots)
},
add (user) {
const bot = new Mastodon({ access_token: user.mastodon_auth.access_token, api_url: `https://${user.instance}/api/v1/` })
const bot = new Mastodon({ access_token: user.mastodon_auth.access_token, api_url: `https://${user.mastodon_instance}/api/v1/` })
const listener = bot.stream('streaming/direct')
listener.on('message', botController.message)
listener.on('error', botController.error)
botController.bots.push({ email: user.email, bot})
botController.bots.push({ email: user.email, bot })
},
post(user, event) {
post (user, event) {
const { bot } = botController.bots.filter(b => b.email === user.email)[0]
const status = `${event.title} @ ${event.place.name} ${moment(event.start_datetime).format('ddd, D MMMM HH:mm')} -
${event.description} - ${event.tags.map(t => '#'+t.tag).join(' ')} ${config.baseurl}/event/${event.id}`
${event.description} - ${event.tags.map(t => '#' + t.tag).join(' ')} ${config.baseurl}/event/${event.id}`
return bot.post('/statuses', { status, visibility: 'private' })
},
async message (msg) {
@@ -44,17 +43,17 @@ ${event.description} - ${event.tags.map(t => '#'+t.tag).join(' ')} ${config.base
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) {
// 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: { }})
// }
// 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)
return
// add comment to specified event
// let comment
//if (!event) {
@@ -69,6 +68,5 @@ ${event.description} - ${event.tags.map(t => '#'+t.tag).join(' ')} ${config.base
}
}
setTimeout(botController.initialize, 2000)
module.exports = botController
+8 -18
View File
@@ -1,7 +1,4 @@
const jwt = require('jsonwebtoken')
const { User, Event, Comment, Tag, Place } = require('../model')
const config = require('../config')
const mail = require('../mail')
const moment = require('moment')
const Sequelize = require('sequelize')
@@ -9,9 +6,9 @@ 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: req.body.id}})
let event = await Event.findOne({ where: { activitypub_id: req.body.id } })
if (!event) {
const comment = await Comment.findOne({where: {activitypub_id: req.body.id}, include: Event})
const comment = await Comment.findOne({ where: { activitypub_id: req.body.id }, include: Event })
event = comment.event
}
const comment = new Comment(req.body)
@@ -19,16 +16,11 @@ const eventController = {
res.json(comment)
},
// async boost (req, res) {
// const event = await Event.findById(req.body.id)
// req.user.addBoost(event)
// res.status(200)
// },
async getMeta(req, res) {
async getMeta (req, res) {
console.log('GET META')
const places = await Place.findAll()
const tags = await Tag.findAll()
res.json({tags, places})
res.json({ tags, places })
},
async updateTag (req, res) {
@@ -41,17 +33,15 @@ const eventController = {
}
},
async get(req, res) {
async get (req, res) {
const id = req.params.event_id
const event = await Event.findByPk(id, { include: [User, Tag, Comment, Place]})
const event = await Event.findByPk(id, { include: [User, Tag, Comment, Place] })
res.json(event)
},
async getAll (req, res) {
const start = moment().year(req.params.year).month(req.params.month).startOf('month').subtract(1, 'week')
const end = moment().year(req.params.year).month(req.params.month).endOf('month').add(1, 'week')
console.log('start', start)
console.log('end', end)
const events = await Event.findAll({
where: {
[Sequelize.Op.and]: [
@@ -63,7 +53,7 @@ const eventController = {
include: [User, Comment, Tag, Place]
})
res.json(events)
},
}
}
+27 -24
View File
@@ -10,12 +10,12 @@ const bot = require('./bot')
const userController = {
async login (req, res) {
// find the user
const user = await User.findOne({where: { email: req.body.email }})
const user = await User.findOne({ where: { email: req.body.email } })
if (!user) {
res.status(404).json({ success: false, message: 'AUTH_FAIL' })
} else if (user) {
if (!user.is_active) {
res.status(403).json({success: false, message: 'NOT)CONFIRMED'})
res.status(403).json({ success: false, message: 'NOT)CONFIRMED' })
}
// check if password matches
else if (!await user.comparePassword(req.body.password)) {
@@ -42,10 +42,9 @@ const userController = {
},
async delEvent (req, res) {
//check if event is mine
// check if event is mine
const event = await Event.findByPk(req.params.id)
if (event && (req.user.is_admin || req.user.id === event.userId))
{
if (event && (req.user.is_admin || req.user.id === event.userId)) {
await event.destroy()
res.sendStatus(200)
} else {
@@ -67,13 +66,13 @@ const userController = {
eventDetails.image_path = req.file.path
}
//create place
// create place
let place
try {
place = await Place.findOrCreate({where: {name: body.place_name},
defaults: {address: body.place_address }})
place = await Place.findOrCreate({ where: { name: body.place_name },
defaults: { address: body.place_address } })
.spread((place, created) => place)
} catch(e) {
} catch (e) {
console.log(e)
}
let event = await Event.create(eventDetails)
@@ -82,15 +81,17 @@ const userController = {
// create/assign tags
console.log(body.tags)
if (body.tags) {
await Tag.bulkCreate(body.tags.map(t => ({ tag: t})), {ignoreDuplicates: true})
const tags = await Tag.findAll({where: { tag: body.tags }})
await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true })
const tags = await Tag.findAll({ where: { tag: body.tags } })
await event.addTags(tags)
}
await req.user.addEvent(event)
event = await Event.findByPk(event.id, {include: [User, Tag, Place]})
event = await Event.findByPk(event.id, { include: [User, Tag, Place] })
// check if bot exists
if (req.user.mastodon_auth) {
const post = await bot.post(req.user, event)
event.activitypub_id = post.id
event.save()
}
return res.json(event)
},
@@ -101,24 +102,26 @@ const userController = {
await event.update(body)
let place
try {
place = await Place.findOrCreate({where: {name: body.place_name},
defaults: {address: body.place_address }})
place = await Place.findOrCreate({ where: { name: body.place_name },
defaults: { address: body.place_address } })
.spread((place, created) => place)
} catch(e) {
console.log('catch', e)
} catch (e) {
console.log('error', e)
}
await event.setPlace(place)
await event.setTags([])
console.log(body.tags)
if (body.tags) {
await Tag.bulkCreate(body.tags.map(t => ({ tag: t})), {ignoreDuplicates: true})
const tags = await Tag.findAll({where: { tag: body.tags }})
await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true })
const tags = await Tag.findAll({ where: { tag: body.tags } })
await event.addTags(tags)
}
const newEvent = await Event.findByPk(event.id, {include: [User, Tag, Place]})
const newEvent = await Event.findByPk(event.id, { include: [User, Tag, Place] })
// check if bot exists
if (req.user.mastodon_auth) {
const post = await bot.post(req.user, newEvent)
event.activitypub_id = post.id
await event.save()
}
return res.json(newEvent)
},
@@ -133,7 +136,7 @@ const userController = {
const { client_id, client_secret } = await Mastodon.createOAuthApp(`https://${instance}/api/v1/apps`, 'eventi', 'read write', `${config.baseurl}/settings`)
const url = await Mastodon.getAuthorizationUrl(client_id, client_secret, `https://${instance}`, 'read write', `${config.baseurl}/settings`)
console.log(req.user)
req.user.instance = instance
req.user.mastodon_instance = instance
req.user.mastodon_auth = { client_id, client_secret }
await req.user.save()
res.json(url)
@@ -142,13 +145,13 @@ const userController = {
async code (req, res) {
const code = req.body.code
const { client_id, client_secret } = req.user.mastodon_auth
const instance = req.user.instance
const instance = req.user.mastodon_instance
try {
const token = await Mastodon.getAccessToken(client_id, client_secret, code, `https://${instance}`, '${config.baseurl}/settings')
const mastodon_auth = { client_id, client_secret, access_token: token}
const token = await Mastodon.getAccessToken(client_id, client_secret, code, `https://${instance}`, `${config.baseurl}/settings`)
const mastodon_auth = { client_id, client_secret, access_token: token }
req.user.mastodon_auth = mastodon_auth
await req.user.save()
await botController.add(token)
await bot.add(token)
res.json(req.user)
} catch (e) {
res.json(e)
+2 -3
View File
@@ -1,10 +1,9 @@
const Sequelize = require('sequelize')
const env = process.env.NODE_ENV || 'development'
const conf = require('../config/config.' + env + '.js')
const conf = require('../config/config.' + env + '.json')
const db = new Sequelize(conf.db)
db.sync({ force: true })
// db.sync({ force: true })
// db.sync()
module.exports = db
+1 -1
View File
@@ -1,6 +1,6 @@
const Email = require('email-templates')
const path = require('path')
const config = require('./config');
const config = require('./config')
const mail = {
send (addresses, template, locals) {
+10 -12
View File
@@ -6,28 +6,28 @@ const Event = db.define('event', {
title: Sequelize.STRING,
description: Sequelize.STRING,
multidate: Sequelize.BOOLEAN,
start_datetime: { type: Sequelize.DATE, index: true},
end_datetime: {type: Sequelize.DATE, index: true},
start_datetime: { type: Sequelize.DATE, index: true },
end_datetime: { type: Sequelize.DATE, index: true },
image_path: Sequelize.STRING,
activitypub_id: { type: Sequelize.INTEGER, index: true },
activitypub_id: { type: Sequelize.INTEGER, index: true }
})
const Tag = db.define('tag', {
tag: { type: Sequelize.STRING, index: true, unique: true, primaryKey: true},
tag: { type: Sequelize.STRING, index: true, unique: true, primaryKey: true },
color: { type: Sequelize.STRING }
})
const Comment = db.define('comment', {
activitypub_id: { type: Sequelize.INTEGER, index: true },
author: Sequelize.STRING,
text: Sequelize.STRING,
text: Sequelize.STRING
})
const MailSubscription = db.define('subscription' , {
const MailSubscription = db.define('subscription', {
filters: Sequelize.JSON,
mail: Sequelize.TEXT,
send_on_add: Sequelize.BOOLEAN,
send_reminder: Sequelize.INTEGER,
send_reminder: Sequelize.INTEGER
})
const Place = db.define('place', {
@@ -38,15 +38,13 @@ const Place = db.define('place', {
Comment.belongsTo(Event)
Event.hasMany(Comment)
Event.belongsToMany(Tag, {through: 'tagEvent'})
Tag.belongsToMany(Event, {through: 'tagEvent'})
Event.belongsToMany(Tag, { through: 'tagEvent' })
Tag.belongsToMany(Event, { through: 'tagEvent' })
Event.belongsToMany(User, {through: 'boost'})
Event.belongsTo(User)
Event.belongsTo(Place)
User.hasMany(Event)
Place.hasMany(Event)
User.belongsToMany(User, {through: 'userFollower', as: 'follower'})
module.exports = { Event, Comment, Tag, Place }
module.exports = { Event, Comment, Tag, Place, MailSubscription }
+5 -4
View File
@@ -5,17 +5,18 @@ const Sequelize = require('sequelize')
const User = db.define('user', {
email: {
type: Sequelize.STRING,
unique: {msg: 'Email already exists'},
index: true, allowNull: false },
unique: { msg: 'Email already exists' },
index: true,
allowNull: false
},
description: Sequelize.TEXT,
password: Sequelize.STRING,
is_admin: Sequelize.BOOLEAN,
is_active: Sequelize.BOOLEAN,
instance: Sequelize.STRING,
mastodon_instance: Sequelize.STRING,
mastodon_auth: Sequelize.JSON
})
User.prototype.comparePassword = async function (pwd) {
if (!this.password) return false
const ret = await bcrypt.compare(pwd, this.password)
+3
View File
@@ -0,0 +1,3 @@
VUE_BASE_URL=https://localhost:8080/
VUE_INSTANCE_API=https://localhost:9000/
VUE_APP_TITLE=Eventi
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
"extends": "standard"
};
+21
View File
@@ -0,0 +1,21 @@
# Gancio
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn run serve
```
### Compiles and minifies for production
```
yarn run build
```
### Lints and fixes files
```
yarn run lint
```
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/app'
]
}
+13
View File
@@ -0,0 +1,13 @@
const env = process.env.NODE_ENV
const beConf = require('../config/config.' + env + '.json')
const conf = {
// environment
title: beConf.title,
// base url
baseurl: beConf.baseurl,
apiurl: beConf.apiurl
}
export default conf
+1
View File
@@ -0,0 +1 @@
.groupMenu[data-v-526ddfcd]{position:absolute;top:40px;width:100%;z-index:2}.badge[data-v-526ddfcd]{cursor:pointer}#tabss ul{-webkit-box-pack:space-evenly;-ms-flex-pack:space-evenly;justify-content:space-evenly;background:linear-gradient(#fff,#fff 22px,#007bff,#fff 23px,#fff)}#tabss ul .nav-link{background-color:#fff}#tabss ul .nav-link.active{background-color:#007bff}#eventDetail .modal-body{padding:0}#eventDetail .card{margin-left:-5px}#eventDetail .badge{margin-left:.1rem}.card-columns .card[data-v-f79fd7b2]{margin-top:.3em;margin-bottom:0}.card-img[data-v-f79fd7b2]{max-height:180px;-o-object-fit:cover;object-fit:cover}.badge[data-v-f79fd7b2]{margin-left:.1rem}#calendar{margin-bottom:0;margin-top:.3em}.card-columns{-webkit-column-count:1;column-count:1;-webkit-column-gap:.3em;column-gap:.3em}@media (min-width:576px){.container{max-width:none}.card-columns{-webkit-column-count:2;column-count:2;-webkit-column-gap:.3em;column-gap:.3em}}@media (min-width:950px){.container{max-width:1400px}.card-columns{-webkit-column-count:3;column-count:3;-webkit-column-gap:.3em;column-gap:.3em}}.item{-webkit-transition:all .2s;transition:all .2s;display:inline-block;width:100%}.list-enter,.list-leave-to{opacity:0;-webkit-transform:translateY(30px);transform:translateY(30px)}.list-leave-active{position:absolute;top:0;width:0;left:0;height:0;z-index:-10}#footer{position:absolute;width:100%;bottom:0}#search,#search ul{-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline}body,html{scrollbar-face-color:#313543;scrollbar-track-color:rgba(0,0,0,.1);font-family:Lato,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:1.1em;color:#2c3e50;background:#000}::-webkit-scrollbar{width:7px;height:7px}::-webkit-scrollbar-thumb{background:#313543;border:0 none #fff;border-radius:6px}::-webkit-scrollbar-thumb:hover{background:#353a49}::-webkit-scrollbar-thumb:active{background:#313543}::-webkit-scrollbar-track{border:0 none #fff;border-radius:6px;background:rgba(0,0,0,.1)}::-webkit-scrollbar-track:active,::-webkit-scrollbar-track:hover{background:#282c37}::-webkit-scrollbar-corner{background:transparent}
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/favicon.ico><title>eventi</title><link href=/css/app.1265c9f1.css rel=preload as=style><link href=/css/chunk-vendors.e4346a46.css rel=preload as=style><link href=/js/app.85eb2b80.js rel=preload as=script><link href=/js/chunk-vendors.f50f5004.js rel=preload as=script><link href=/css/chunk-vendors.e4346a46.css rel=stylesheet><link href=/css/app.1265c9f1.css rel=stylesheet></head><body><noscript><strong>We're sorry but eventi doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/js/chunk-vendors.f50f5004.js></script><script src=/js/app.85eb2b80.js></script></body></html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+66
View File
@@ -0,0 +1,66 @@
{
"name": "eventi",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.18.0",
"bootstrap-vue": "^2.0.0-rc.11",
"element-ui": "^2.5.4",
"mastodon-api": "^1.3.0",
"moment": "^2.23.0",
"node-sass": "^4.11.0",
"npm": "^6.8.0",
"postcss-flexbugs-fixes": "^4.1.0",
"pug": "^2.0.3",
"sass-loader": "^7.1.0",
"v-calendar": "^0.9.7",
"vue": "^2.5.17",
"vue-awesome": "^3.3.1",
"vue-clipboard2": "^0.3.0",
"vue-i18n": "^8.5.0",
"vue-router": "^3.0.1",
"vuex": "^3.0.1",
"vuex-persist": "^2.0.0",
"weekstart": "^1.0.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.2.0",
"@vue/cli-plugin-eslint": "^3.2.0",
"@vue/cli-service": "^3.2.0",
"@vue/eslint-config-standard": "^4.0.0",
"babel-eslint": "^10.0.1",
"eslint": "^5.8.0",
"eslint-plugin-vue": "^5.0.0-0",
"pug-plain-loader": "^1.0.0",
"vue-template-compiler": "^2.5.17"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"@vue/standard"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+17
View File
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>Gancio</title>
</head>
<body>
<noscript>
<strong>We're sorry but eventi doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
+139
View File
@@ -0,0 +1,139 @@
<template lang='pug'>
#app
b-navbar(type="dark" variant="dark" toggleable='lg')
b-navbar-brand(to='/') Gancio
b-navbar-toggle(target='nav_collapse')
b-collapse#nav_collapse(is-nav)
b-navbar-nav.ml-auto(v-if='logged')
b-nav-item(to='/new_event') <v-icon color='lightgreen' name='plus'/> {{$t('Add Event')}}
b-nav-item(@click='search=!search') <v-icon color='lightgreen' name='search'/> {{$t('Search')}}
b-nav-item(to='/settings') <v-icon color='orange' name='cog'/> {{$t('Settings')}}
b-nav-item(v-if='user.is_admin' to='/admin') <v-icon color='lightblue' name='tools'/> {{$t('Admin')}}
b-nav-item(variant='danger' @click='logout') <v-icon color='red' name='sign-out-alt'/> {{$t('Logout')}}
b-navbar-nav.ml-auto(v-else)
b-nav-item(@click='search=!search') <v-icon color='lightgreen' name='search'/> {{$t('Search')}}
b-nav-item(to='/register') {{$t('Register')}}
b-nav-item(to='/login') {{$t('Login')}}
transition(name='toggle')
b-navbar#search(v-if='search' type='dark' variant="dark" toggleable='lg')
b-navbar-nav
b-nav-form
typeahead.ml-1(v-model='filters_places'
textField='name' valueField='name'
updateOnMatchOnly
:data='places' multiple placeholder='Luogo')
b-nav-form
typeahead.ml-1(v-model='filters_tags'
updateOnMatchOnly
textField='tag' valueField='tag'
:data='tags' multiple placeholder='Tags')
b-navbar-nav.ml-auto(variant='dark')
b-nav-item(to='/export/feed' href='#') <v-icon color='orange' name='rss'/> feed
b-nav-item(to='/export/ics') <v-icon color='orange' name='calendar'/> cal
b-nav-item(to='/export/email') <v-icon color='orange' name='envelope'/> mail
b-nav-item(to='/export/embed') <v-icon color='orange' name='code'/> embed
b-nav-item(to='/export/print') <v-icon color='orange' name='print'/> print
Home
transition(name="fade" mode="out-in")
router-view(name='modal')
</template>
<script>
import moment from 'moment'
import api from '@/api'
import { mapActions, mapState } from 'vuex';
import Register from '@/components/Register'
import Login from '@/components/Login'
import Settings from '@/components/Settings'
import newEvent from '@/components/newEvent'
import eventDetail from '@/components/EventDetail'
import Timeline from '@/components/Timeline'
import Home from '@/components/Home'
export default {
name: 'App',
mounted () {
this.updateMeta()
},
data () {
return {search: false}
},
components: { Register, Login, Home, Settings, newEvent, eventDetail },
computed: {
...mapState(['logged', 'user', 'filters', 'tags', 'places']),
filters_tags: {
set (value) {
console.log('dentro set ', value)
this.setSearchTags(value)
},
get () {
console.log('dentro get')
console.log(this.filters)
return this.filters.tags
}
},
filters_places: {
set (value) {
this.setSearchPlaces(value)
},
get () {
return this.filters.places
}
}
},
methods: mapActions(['logout', 'updateMeta', 'addSearchTag',
'setSearchTags', 'setSearchPlaces', 'addSearchPlace']),
}
</script>
<style>
#footer {
position: absolute;
width: 100%;
bottom: 0px;
}
#search,
#search ul {
align-items: baseline;
}
html, body {
scrollbar-face-color: #313543;
scrollbar-track-color: rgba(0, 0, 0, 0.1);
font-family: Lato,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;
font-size: 1.1em;
color: #2c3e50;
background: black;
}
::-webkit-scrollbar {
width: 7px;
height: 7px; }
::-webkit-scrollbar-thumb {
background: #313543;
border: 0px none #ffffff;
border-radius: 6px; }
::-webkit-scrollbar-thumb:hover {
background: #353a49; }
::-webkit-scrollbar-thumb:active {
background: #313543; }
::-webkit-scrollbar-track {
border: 0px none #ffffff;
border-radius: 6px;
background: rgba(0, 0, 0, 0.1); }
::-webkit-scrollbar-track:hover {
background: #282c37; }
::-webkit-scrollbar-track:active {
background: #282c37; }
::-webkit-scrollbar-corner {
background: transparent; }
/* .column {
margin-top: 3px;
margin-right: 3px;
margin-bottom: 3px;
width: 350px;
} */
</style>
+47
View File
@@ -0,0 +1,47 @@
import axios from 'axios'
import config from '../config'
import store from './store'
const api = axios.create({
baseURL: config.apiurl,
withCredentials: false,
responseType: 'json',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
function get (path) {
return api.get(path, { headers: { 'x-access-token': store.state.token } }).then(ret => ret.data)
}
function post (path, data) {
return api.post(path, data, { headers: { 'x-access-token': store.state.token } }).then(ret => ret.data)
}
function put (path, data) {
return api.put(path, data, { headers: { 'x-access-token': store.state.token } }).then(ret => ret.data)
}
function del (path) {
console.log(store.state.token)
return api.delete(path, { headers: { 'x-access-token': store.state.token } }).then(ret => ret.data)
}
export default {
login: (email, password) => post('/login', { email, password }),
register: user => post('/user', user),
getAllEvents: (month, year) => get(`/event/${year}/${month}/`),
addEvent: event => post('/user/event', event),
updateEvent: event => put('/user/event', event),
delEvent: eventId => del(`/user/event/${eventId}`),
getEvent: eventId => get(`/event/${eventId}`),
getMeta: () => get('/event/meta'),
getUser: () => get('/user'),
getUsers: () => get('/users'),
updateTag: (tag) => put('/tag', tag),
updateUser: user => put('/user', user),
getAuthURL: mastodonInstance => post('/user/getauthurl', mastodonInstance),
setCode: code => post('/user/code', code),
getKnowLocations: () => get('/locations'),
getKnowTags: () => get('/tags')
}
+5
View File
@@ -0,0 +1,5 @@
/* html, body {
max-height: 100%;
overflow: hidden;
} */
+67
View File
@@ -0,0 +1,67 @@
<template lang="pug">
b-modal(hide-footer hide-header
@hide='$router.go(-1)' size='lg' :visible='true')
h4.text-center Admin
b-tabs(pills)
b-tab
template(slot='title')
v-icon(name='users')
span {{$t('Users')}}
b-table(:items='users' :fields='userFields' striped hover)
template(slot='action' slot-scope='data')
b-button.mr-1(:variant='data.item.is_active?"warning":"success"' @click='toggle(data.item)') {{data.item.is_active?$t('Deactivate'):$t('Activate')}}
b-button(:variant='data.item.is_admin?"danger":"warning"' @click='toggleAdmin(data.item)') {{data.item.is_admin?$t('Remove Admin'):$t('Admin')}}
b-tab
template(slot='title')
v-icon(name='map-marker-alt')
span {{$t('Places')}}
b-table(:items='places' :fields='placeFields' striped hover)
b-tab
template(slot='title')
v-icon(name='tag')
span {{$t('Tags')}}
b-table(:items='tags' :fields='tagFields' striped hover)
template(slot='tag' slot-scope='data')
b-badge(:style='{backgroundColor: data.item.color}') {{data.item.tag}}
template(slot='color' slot-scope='data')
el-color-picker(v-model='data.item.color' @change='updateColor(data.item)')
b-tab
template(slot='title')
v-icon(name='tools')
span {{$t('Settings')}}
</template>
<script>
import { mapState } from 'vuex'
import api from '@/api'
export default {
name: 'Admin',
data () {
return {
users: [],
userFields: ['email', 'action'],
placeFields: ['name', 'address'],
tagFields: ['tag', 'color'],
description: '',
}
},
async mounted () {
this.users = await api.getUsers()
},
computed: mapState(['tags', 'places']),
methods: {
async toggle(user) {
user.is_active = !user.is_active
const newuser = await api.updateUser(user)
},
async toggleAdmin(user) {
user.is_admin = !user.is_admin
const newuser = await api.updateUser(user)
},
async updateColor(tag) {
const newTag = await api.updateTag(tag)
}
}
}
</script>
+82
View File
@@ -0,0 +1,82 @@
<template lang="pug">
v-calendar#calendar.card(
:attributes='attributes'
:from-page.sync='page'
is-expanded is-inline)
div(slot='popover', slot-scope='{ customData }')
router-link(:to="`/event/${customData.id}`") {{customData.start_datetime|hour}} - {{customData.title}} @{{customData.place.name}}
</template>
<script>
import { mapState, mapActions } from 'vuex'
import filters from '@/filters'
import moment from 'moment'
export default {
name: 'Calendar',
filters,
data () {
const month = moment().month()+1
const year = moment().year()
return {
page: { month, year},
}
},
mounted () {
this.updateEvents(this.page)
},
watch: {
page () {
this.updateEvents(this.page)
}
},
methods: {
...mapActions(['updateEvents']),
eventToAttribute(event) {
let e = {
key: event.id,
customData: event,
order: event.start_datetime,
popover: {
slot: 'popover',
visibility: 'hover'
}
}
const color = event.tags.length && event.tags[0].color ? event.tags[0].color : 'rgba(200,200,200,0.5)'
if (event.multidate) {
e.dates = {
start: event.start_datetime, end: event.end_datetime
}
e.highlight = { backgroundColor: color,
borderColor: 'transparent',
borderWidth: '4px' }
} else {
e.dates = event.start_datetime
e.dot = { backgroundColor: color, borderColor: color, borderWidth: '3px' }
}
return e
}
},
computed: {
...mapState(['events', 'tags']),
attributes () {
return [
{ key: 'todaly', dates: new Date(),
highlight: {
backgroundColor: '#aaffaa'
},
popover: {label: this.$t('Today')}
},
...this.events.map(this.eventToAttribute)
]
}
}
}
</script>
<style>
#calendar {
margin-bottom: 0em;
margin-top: 0.3em;
}
</style>
+60
View File
@@ -0,0 +1,60 @@
<template lang="pug">
b-card(bg-variant='dark' text-variant='white'
@click='$router.push("/event/" + event.id)'
:img-src='imgPath')
h4 {{event.title}}
div <v-icon name='clock'/> {{event.start_datetime|datetime}}
span(v-b-popover.hover="event.place && event.place.address || ''") <v-icon name='map-marker-alt'/> {{event.place.name}}
br
b-badge(:style='{backgroundColor: tag.color}' v-for='tag in event.tags' href='#'
@click.stop='addSearchTag(tag)') {{tag.tag}}
</template>
<script>
import { mapState, mapActions } from 'vuex';
import api from '@/api'
import filters from '@/filters'
import config from '../../config'
export default {
props: ['event'],
computed: {
...mapState(['user']),
imgPath () {
return this.event.image_path && config.apiurl + '/../' + this.event.image_path
},
mine () {
return this.event.userId === this.user.id
}
},
filters,
methods: {
...mapActions(['delEvent', 'addSearchTag']),
async remove () {
await api.delEvent(this.event.id)
this.delEvent(this.event.id)
}
}
}
</script>
<style scoped>
/* .card::before {
border-top: 4px solid black;
content: ''
} */
.card-columns .card {
margin-top: 0.3em;
margin-bottom: 0em;
}
.card-img {
max-height: 180px;
object-fit: cover;
}
.badge {
margin-left: 0.1rem;
}
</style>
+91
View File
@@ -0,0 +1,91 @@
<template lang="pug">
b-modal#eventDetail(hide-footer hide-header
@hide='$router.go(-1)' size='lg' :visible='true')
b-card(bg-variant='dark' href='#' text-variant='white'
no-body, :img-src='imgPath')
b-card-header
h3 {{event.title}}
v-icon(name='clock')
span {{event.start_datetime|datetime}}
br
v-icon(name='map-marker-alt')
span {{event.place.name}} - {{event.place.address}}
br
b-card-footer(v-if='event.description || event.tags') {{event.description}}
br
b-badge(:style='{backgroundColor: tag.color}' v-for='tag in event.tags') {{tag.tag}}
b-navbar(v-if='mine' type="dark" variant="dark" toggleable='lg')
b-navbar-nav.ml-auto
b-nav-item(@click.prevent='remove') <v-icon color='red' name='times'/> {{$t('Remove')}}
b-nav-item(:to='"/edit/"+event.id') <v-icon color='orange' name='edit'/> {{$t('Edit')}}
//- b-card-footer.text-right
//- span.mr-3 {{event.comments.length}} <v-icon name='comments'/>
//- a(href='#', @click='remove')
v-icon(color='orange' name='times')
//- b-card-footer(v-for='comment in event.comments')
strong {{comment.author}}
div(v-html='comment.text')
</template>
<script>
import { mapState, mapActions } from 'vuex';
import api from '@/api'
import filters from '@/filters'
import config from '../../config'
export default {
computed: {
...mapState(['user']),
imgPath () {
return this.event.image_path && config.apiurl + '/../' + this.event.image_path
},
mine () {
return this.event.userId === this.user.id || this.user.is_admin
}
},
data () {
return {
event: { comments: [], place: {}},
id: null,
}
},
mounted () {
this.id = this.$route.params.id
this.load()
},
filters: filters,
methods: {
...mapActions(['delEvent']),
async load () {
const event = await api.getEvent(this.id)
this.event = event
},
async remove () {
await api.delEvent(this.event.id)
this.delEvent(this.event.id)
this.$router.go(-1)
}
}
}
</script>
<style>
#eventDetail .modal-body {
padding: 0px;
}
/* .card::before {
border-top: 4px solid black;
content: ''
} */
#eventDetail .card {
margin-left: -5px;
}
/* #eventDetail .card-img {
max-height: 150px;
object-fit: cover;
} */
#eventDetail .badge {
margin-left: 0.1rem;
}
</style>
+60
View File
@@ -0,0 +1,60 @@
<template lang="pug">
b-modal(hide-footer hide-header
@hide='$router.go(-1)' size='lg' :visible='true' v-if='type')
h3.text-center Export {{type}}
b-input-group.mb-2(v-if='showLink')
b-form-input( v-model='link' autocomplete='off')
b-input-group-append
b-button(variant="success" v-clipboard:copy="link") <v-icon name='clipboard'/> Copy
p {{$t('export_intro')}}
p(v-html='$t(`export_${type}_explanation`)')
li(v-if='filters.tags.length') {{$t('Tags')}} ->
b-badge.ml-1(v-for='tag in filters.tags') {{tag}}
li(v-if='filters.places.length') {{$t('Places')}}
b-badge.ml-1(v-for='place in filters.places') {{place}}
b-form(v-if="type==='email'")
el-switch(v-model='mail.sendOnInsert' :active-text="$t('notify_on_insert')")
br
el-switch(v-model='mail.reminder' :active-text="$t('send_reminder')")
b-form-input.mt-1(v-model='mail.mail' :placeholder="$t('Insert your address')")
b-button.mt-1.float-right(variant='success' @click='activate_email') {{$t('Send')}}
</template>
<script>
import { mapState } from 'vuex'
import config from '../../config'
import path from 'path'
export default {
name: 'Export',
data () {
return {
type: '',
link: '',
mail: {}
}
},
mounted () {
this.type = this.$route.params.type
this.link = this.loadLink()
if (this.type === 'email' && this.logged) {
this.mail.mail = this.user.email
}
},
methods: {
activate_email () {
this.$router.go(-1)
},
loadLink () {
const filters = this.filters.tags.join(',')
return `${config.apiurl}/export/${this.type}/${filters}`
}
},
computed: {
...mapState(['filters', 'user', 'logged']),
showLink () {
return (['feed', 'ics'].indexOf(this.type)>-1)
},
}
}
</script>
+81
View File
@@ -0,0 +1,81 @@
<template lang="pug">
b-container
b-card-group(columns)
Calendar
//- transition-group(name="list" tag="div")
Event.item(v-for='event in filteredEvents'
:key='event.id'
:event='event')
</template>
<script>
import { mapState } from 'vuex'
import filters from '@/filters.js'
import Event from '@/components/Event'
import Calendar from '@/components/Calendar'
import {intersection} from 'lodash'
export default {
name: 'Home',
components: { Event, Calendar },
computed: {
...mapState(['events', 'filters']),
filteredEvents () {
if (!this.filters.tags.length && !this.filters.places.length) return this.events
return this.events.filter(e => {
if (this.filters.tags.length) {
const m = intersection(e.tags.map(t => t.tag), this.filters.tags)
if (m.length>0) return true
}
if (this.filters.places.length) {
if (this.filters.places.find(p => p === e.place.name))
return true
}
return 0
})
}
}
}
</script>
<style>
.card-columns {
column-count: 1;
column-gap: 0.3em;
}
@media (min-width: 576px) {
.container {
max-width: none;
}
.card-columns {
column-count: 2;
column-gap: 0.3em;
}
}
@media (min-width: 950px) {
.container {
max-width: 1400px;
}
.card-columns {
column-count: 3;
column-gap: 0.3em;
}
}
.item {
transition: all .2s;
display: inline-block;
width: 100%;
}
.list-enter, .list-leave-to {
opacity: 0;
transform: translateY(30px);
}
.list-leave-active {
position: absolute;
top: 0px;
width: 0px;
left: 0px;
height: 0px;
z-index: -10;
}
</style>
+47
View File
@@ -0,0 +1,47 @@
<template lang='pug'>
b-modal(hide-header hide-footer @shown="$refs.email.focus()"
@hide='$router.go(-1)' :visible='true')
h4.text-center.center {{$t('Login')}}
b-form
//- p.text-muted Sign In to your account
b-input-group.mb-1
b-input-group-prepend
b-input-group-text
v-icon(name="user")
b-form-input(ref='email' v-model="email" type="text" class="form-control" placeholder="E-mail" autocomplete="email")
b-input-group.mb-3
b-input-group-prepend
b-input-group-text
v-icon(name="lock")
b-form-input(v-model="password" type="password" class="form-control" placeholder="Password" autocomplete="current-password")
b-button.float-right(variant="success" @click='submit') Login
</template>
<script>
import api from '@/api'
import { mapActions } from 'vuex'
import { log } from 'util';
export default {
name: 'Login',
data () {
return {
password: '',
email: '',
}
},
methods: {
...mapActions(['login']),
async submit (e) {
e.preventDefault()
const user = await api.login(this.email, this.password)
if (!user) {
return;
}
this.login(user)
this.email = this.password = ''
this.$router.go(-1)
}
}
}
</script>
+58
View File
@@ -0,0 +1,58 @@
<template lang='pug'>
b-modal(hide-header hide-footer
@hide='$router.go(-1)' :visible='true' @shown='$refs.email.focus()')
h4.text-center.center {{$t('Register')}}
b-form
p.text-muted(v-html="$t('register_explanation')")
b-input-group.mb-1
b-input-group-prepend
b-input-group-text @
b-form-input(ref='email' v-model='user.email' type="text" class="form-control" placeholder="Email" autocomplete="email" )
b-input-group.mb-1
b-input-group-prepend
b-input-group-text
v-icon(name='lock')
b-form-input(v-model='user.password' type="password" class="form-control" placeholder="Password")
b-input-group.mb-1
b-input-group-prepend
b-input-group-text
v-icon(name='envelope-open-text')
b-form-textarea(v-model='user.description' type="text" rows='3' class="form-control" :placeholder="$t('Description')")
b-button.float-right(variant="success" @click='register') {{$t('Send')}}
</template>
<script>
import api from '@/api'
import { mapActions } from 'vuex';
export default {
name: 'Register',
data () {
return {
error: {},
user: { }
}
},
methods: {
...mapActions(['login']),
async register () {
try {
const user = await api.register(this.user)
// this.login(user)
// this.user = { name: '' }
this.$router.go(-1)
this.$message({
message: this.$t('registration_complete'),
type: 'success'
});
} catch (e) {
console.error(e)
}
}
}
}
</script>
+44
View File
@@ -0,0 +1,44 @@
<template lang="pug">
b-modal(hide-header hide-footer @hide='$router.push("/")' :visible='true')
h4.text-center {{$t('Settings')}}
b-form
b-input-group.mt-1(prepend='Email')
b-form-input(v-model="user.email")
//- b-form-checkbox(v-model="tmpUser.user.autoboost") Autoboost
b-input-group.mt-1(prepend='Mastodon instance')
b-form-input(v-model="mastodon_instance")
b-input-group-append
b-button(@click='associate', variant='primary') Associate
</template>
<script>
import { mapState, mapActions } from 'vuex'
import api from '@/api'
export default {
props: ['code'],
data () {
return {
mastodon_instance: '',
user: {}
}
},
computed: mapState(['oauth']),
async mounted () {
const code = this.$route.query.code
if (code) {
const res = await api.setCode({code})
}
const user = await api.getUser()
this.user = user
this.mastodon_instance = user.instance
},
methods: {
async associate () {
const url = await api.getAuthURL({instance: this.mastodon_instance})
setTimeout( () => window.location.href=url, 100);
}
}
}
</script>
+17
View File
@@ -0,0 +1,17 @@
<template lang="pug">
b-card.column.pl-1(bg-variant='dark' text-variant='white' no-body)
b-card-header
strong Public events
b-btn.float-right(v-if='logged' variant='success' size='sm' to='/newEvent') <v-icon name="plus"/> Add Event
event(v-for='event in events', :event='event' :key='event.id')
</template>
<script>
import api from '@/api'
import event from './Event'
import { mapState } from 'vuex';
export default {
components: {event},
computed: mapState(['events', 'logged'])
}
</script>
+240
View File
@@ -0,0 +1,240 @@
<template lang='pug'>
div(style="position: relative")
b-input-group
input.form-control(type="search"
ref="input"
:placeholder="placeholder"
v-model="search"
@input="update"
autocomplete="off"
@keydown.backspace="backspace"
@keydown.up.prevent="up"
@keydown.down.prevent="down"
@keydown.enter="hit"
@keydown.esc="reset(true)"
@blur="focus = false"
@focus="focus = true")
div
b-badge.mr-1(@click="removeSelected(sel)"
v-for="sel in selectedLabel"
:key="sel") {{sel}}
b-list-group.groupMenu(v-show='showDropdown')
b-list-group-item(:key="$index" v-for="(item, $index) in matched"
href='#'
:class="{'active': isActive($index)}"
@mousedown.prevent="hit"
@mousemove="setActive($index)")
slot(:name="templateName") {{textField ? item[textField] : item}}
</template>
<script>
export default {
props: {
value: {
twoWay : true,
type: [String, Array, Set],
default: ''
},
data: {
type: Array
},
template: {
type: String
},
templateName: {
type: String,
default: 'default'
},
valueField: {
type: String,
default: null
},
textField: {
type: String,
default: null
},
showClear: {
type: Boolean,
default: true
},
matchCase: {
type: Boolean,
default: false
},
matchStart: {
type: Boolean,
default: false
},
onHit: {
type: Function,
default () {
this.reset()
}
},
placeholder: {
type: String
},
updateOnMatchOnly: {
type: Boolean,
default: false,
},
multiple: {
type: Boolean,
default: false
},
maxMatch: {
type: Number,
default: 4
}
},
data () {
return {
focus: false,
noResults: true,
current: 0,
search: '',
selected: [],
}
},
watch: {
value(newValue) {
if (!newValue) {
this.search = '';
if (this.multiple) this.$emit('input', []) // this.selected = [];
} else {
if (!this.multiple) {
this.search = newValue
} else {
this.selected = newValue
}
}
}
},
computed: {
showDropdown () {
return this.focus
},
selectedValues () {
return this.selected.map(s => this.valueField ? (s[this.valueField] || s) : s);
},
selectedLabel () {
return this.selected.map(s => this.textField ? s[this.textField] || s: s);
},
matched () {
if (this.data) {
return this.data.filter(value => {
if(this.textField) value = value[this.textField];
if (this.multiple && this.selectedLabel.includes(value)) return false;
value = this.matchCase ? value : value.toLowerCase()
const query = this.matchCase ? this.search : this.search.toLowerCase()
return this.matchStart ? value.indexOf(query) === 0 : value.indexOf(query) !== -1
}).slice(0, this.maxMatch)
}
}
},
methods: {
update (e) {
if (this.multiple && e.data === ',') {
this.search = this.search.substr(0, this.search.length-1)
this.hit(e)
return
}
if (!this.updateOnMatchOnly && !this.multiple) this.$emit('input', this.search);
this.focus = true;
if (!this.matched.length) {
this.focus = false;
this.current = 0
return;
}
// current selected item has to be in the match
if (this.matched.length <= this.current) {
this.current = this.matched.length-1;
}
},
backspace () {
if (this.search) return
this.selected.splice(-1, 1)
this.$emit('input', this.selected.length ? this.selectedValues : '');
},
reset (esc=false) {
this.search = '';
this.current = 0;
this.$refs.input.focus();
if (esc) {
this.focus = false
} else {
this.selected = [];
this.$emit('input', '');
}
},
setActive (index) {
this.current = index
},
isActive (index) {
return this.current === index
},
removeSelected (label) {
this.selected = this.selected.filter( s => (this.textField ? s[this.textField] || s: s) !== label);
this.$emit('input', this.selected.length ? this.selectedValues : []);
},
// click or enter on curren item
hit (e) {
e.preventDefault();
let code = '';
let item = '';
if (this.matched.length !== 0 && this.focus) {
item = this.matched[this.current];
code = this.textField ? item[this.textField] : item;
// code = this.valueField ? item[this.valueField] : item;
} else {
code = this.search;
}
if (this.multiple) {
if (code) {
this.selected.push(code);
this.search = '';
this.$emit('input', this.selected);
this.focus = false;
// this.update();
}
} else {
this.$emit('input', code);
this.current = 0;
this.focus = false;
this.search = code
}
this.$emit('enter')
},
// manage up/down arrow key
up () {
if (this.current > 0) this.current--
},
down () {
if (this.current < this.matched.length - 1) this.current++
}
},
}
</script>
<style scoped>
.groupMenu {
position: absolute;
top: 40px;
width: 100%;
z-index: 2;
}
.badge {
cursor: pointer;
}
</style>
+9
View File
@@ -0,0 +1,9 @@
import Register from '@/components/Register'
import Login from '@/components/Login'
import Settings from '@/components/Settings'
import newEvent from '@/components/newEvent'
import eventDetail from '@/components/EventDetail'
import Home from '@/components/Home'
import Event from '@/components/event'
module.exports = { Home, eventDetail, newEvent, Settings, Login, Register, Event }
+206
View File
@@ -0,0 +1,206 @@
<template lang="pug">
b-modal(hide-header hide-footer no-close-on-backdrop
@hide='$router.go(-1)' no-close-on-esc size='lg' :visible='true')
h4.text-center.center {{edit?$t('Edit event'):$t('New event')}}
b-tabs#tabss(pills v-model='activeTab')
b-form
b-tab
template(slot='title')
v-icon(name='map-marker-alt')
span {{$t('Where')}}
b-card-body
span.text-muted {{$t('where_explanation')}}
typeahead.mb-3(v-model='event.place.name' :data='places_name' @enter='placeChoosed')
span.text-muted {{$t('address_explanation')}}
b-form-input(ref='address' v-model='event.place.address' @keydown.native.enter='next')
b-tab
template(slot='title')
v-icon(name='clock')
span {{$t('When')}}
b-card-body
el-switch.float-right(v-model='event.multidate' :active-text="$t('multidate_explanation')")
span.text-muted {{event.multidate ? $t('dates_explanation') : $t('date_explanation')}}
v-date-picker.mb-3(:mode='event.multidate ? "range" : "single"' v-model='date' is-inline
is-expanded :min-date='new Date()' @input='date ? $refs.time_start.focus() : false')
b-row
b-col
label.text-muted {{$t('time_start_explanation')}}
el-time-select(ref='time_start'
v-model="time.start"
:picker-options="{ start: '00:00', step: '00:30', end: '24:00'}")
b-col.text-right
label.text-muted {{$t('time_end_explanation')}}
el-time-select(
v-model='time.end'
:picker-options="{start: `${event.multidate?'00:00':time.start}`, step: '00:30', end: '24:00'}"
)
b-tab
template(slot='title')
v-icon(name='file-alt')
span {{$t('What')}}
b-card-body
span.text-muted {{$t('what_explanation')}}
b-form-input.mb-3(v-model.trim='event.title' autocomplete='off')
span.text-muted {{$t('description_explanation')}}
b-form-textarea.mb-3(v-model='event.description' :rows='3')
span.text-muted {{$t('tag_explanation')}}
typeahead(v-model="event.tags" :data='tags' multiple)
b-tab
template(slot='title')
v-icon(name='image')
span {{$t('Media')}}
b-card-body
span.text-muted {{$t('media_explanation')}}
b-form-file(v-model='event.image', :placeholder='$t("Poster")' accept='image/*')
b-button(v-if='activeTab==0' variant='danger' @click='$router.go(-1)') {{$t('Cancel')}}
b-button.float-left(v-else variant='danger' @click='prev') {{$t('Prev')}}
b-button.float-right(v-if='activeTab<3' variant='success' @click='next' :disabled='!couldProceed') {{$t('Next')}}
b-button.float-right(v-else variant='success' @click='done') {{edit?$t('Edit'):$t('Send')}}
</template>
<script>
import api from '@/api'
import { mapActions, mapState } from 'vuex'
import moment from 'moment'
export default {
data() {
return {
event: {
place: { name: '', address: '' },
title: '', description: '', tags: [],
multidate: false,
},
id: null,
activeTab: 0,
date: null,
time: { start: '00:00', end: null },
edit: false
}
},
name: 'newEvent',
async mounted () {
if (this.$route.params.id) {
this.id = this.$route.params.id
this.edit = true
const event = await api.getEvent(this.id)
// this.event.place = {name: event.place.name, address: event.place.address }
this.event.place.name = event.place.name
this.event.place.address = event.place.address || ''
this.event.multidate = event.multidate
this.date = event.start_datetime
this.time.start = moment(event.start_datetime).format('HH:mm')
this.time.end = moment(event.end_datetime).format('HH:mm')
this.event.title = event.title
this.event.description = event.description
this.event.id = event.id
if (event.tags) {
this.event.tags = event.tags.map(t => t.tag)
}
}
this.updateMeta()
},
computed: {
...mapState({
tags: state => state.tags.map(t => t.tag ),
places_name: state => state.places.map(p => p.name ),
places: state => state.places
}),
couldProceed () {
switch(this.activeTab) {
case 0:
return this.event.place.name.length>0 &&
this.event.place.address.length>0
case 1:
return true
break
case 2:
return this.event.title.length>0
break
case 3:
return true
break
}
}
},
methods: {
...mapActions(['addEvent', 'updateEvent', 'updateMeta']),
next () {
this.activeTab++
},
prev () {
this.activeTab--
},
placeChoosed () {
const place = this.places.find( p => p.name === this.event.place.name )
if (place && place.address) {
this.event.place.address = place.address
}
this.$refs.address.focus()
},
async done () {
let start_datetime, end_datetime
const [ start_hour, start_minute ] = this.time.start.split(':')
if (!this.time.end) {
this.time.end = this.time.start
}
const [ end_hour, end_minute ] = this.time.end.split(':')
if (this.event.multidate) {
start_datetime = moment(this.date.start)
.hour(start_hour).minute(start_minute)
end_datetime = moment(this.date.end)
.hour(end_hour).minute(end_minute)
} else {
start_datetime = moment(this.date)
.hour(start_hour).minute(start_minute)
end_datetime = moment(this.date)
.hour(end_hour).minute(end_minute)
}
const formData = new FormData()
if (this.event.image) {
formData.append('image', this.event.image, this.event.image.name)
}
formData.append('title', this.event.title)
formData.append('place_name', this.event.place.name)
formData.append('place_address', this.event.place.address)
formData.append('description', this.event.description)
formData.append('multidate', this.event.multidate)
formData.append('start_datetime', start_datetime)
formData.append('end_datetime', end_datetime)
if (this.edit) {
formData.append('id', this.event.id)
}
if (this.event.tags)
this.event.tags.forEach(tag => formData.append('tags[]', tag))
try {
if (this.edit) {
await this.updateEvent(formData)
} else {
await this.addEvent(formData)
}
this.updateMeta()
this.$router.go(-1)
} catch (e) {
console.error(e)
}
}
}
}
</script>
<style scope>
#tabss ul {
justify-content: space-evenly;
background: linear-gradient( #fff, #FFF 22px, #007bff, #fff 23px, #fff)
}
#tabss ul .nav-link {
background-color: white;
}
#tabss ul .nav-link.active {
background-color: #007bff;
}
</style>
+11
View File
@@ -0,0 +1,11 @@
import moment from 'moment'
moment.locale('it')
export default {
datetime (value) {
return moment(value).format('ddd, D MMMM HH:mm')
},
hour (value) {
return moment(value).format('HH:mm')
}
}
+29
View File
@@ -0,0 +1,29 @@
const en = {
where_explanation: 'Specify event\' place',
address_explanation: 'Insert address',
multidate_explanation: 'Multiple date?',
when_explanation: 'Select a day',
what_explanation: 'Event\'s title',
description_explanation: 'Describe the event',
date_explanation: 'Select the day',
dates_explanation: 'Select the days',
time_start_explanation: 'Insert start time',
time_end_explanation: 'You could insert end time',
media_explanation: 'You can upload a media',
tag_explanation: 'Insert a tag',
export_intro: `Sharing is caring.`,
export_feed_explanation: `Per seguire gli aggiornamenti da computer o smartphone senza la necessità di aprire periodicamente il sito, il metodo consigliato è quello dei Feed RSS.<br/>
Con i feed rss utilizzi un'apposita applicazione per ricevere aggiornamenti dai siti che più ti interessano.
È un buon metodo per seguire anche molti siti in modo molto rapido, senza necessità di creare un account o altre complicazioni.<br/>
Se hai Android, ti consigliamo <a href="https://play.google.com/store/apps/details?id=net.frju.flym">Flym</a> o Feeder<br />
Per iPhone/iPad puoi usare <a href="https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8">Feed4U</a><br />
Per il computer fisso/portatile consigliamo Feedbro, da installare all'interno <a href="https://addons.mozilla.org/en-GB/firefox/addon/feedbroreader/">di Firefox </a>o <a href="https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa">di Chrome</a> e compatibile con tutti i principali sistemi operativi.</p>
<br/>
Aggiungendo il link sopra, rimarrai aggiornata sui seguenti eventi:`,
SignIn: 'Sign In',
registration_email: `Hi, your registration will be confirmed soon.`,
register_explanation: `..`
}
module.exports = en
+63
View File
@@ -0,0 +1,63 @@
const it = {
'Add Event': 'Nuovo evento',
Where: 'Dove',
When: 'Quando',
What: 'Cosa',
Media: 'Locandina',
where_explanation: 'Specifica il luogo dell\'evento',
address_explanation: 'Inserisci l\'indirizzo',
multidate_explanation: 'Dura tanti giorni?',
when_explanation: 'Seleziona un giorno',
what_explanation: 'Titolo dell\'evento (es. Corteo Antimilitarista),',
description_explanation: 'Descrivi l\'evento, dajene di copia/incolla',
date_explanation: 'Seleziona il giorno',
dates_explanation: 'Seleziona i giorni',
time_start_explanation: 'Inserisci un orario di inizio',
time_end_explanation: 'Puoi inserire un orario di fine',
media_explanation: 'Se vuoi puoi mettere una locandina/manifesto',
tag_explanation: 'Puoi inserire un tag (es. concerto, corteo)',
export_intro: `Contrariamente alle piattaforme del capitalismo, che fanno di tutto per tenere
i dati e gli utenti al loro interno, crediamo che le informazioni, come le persone,
debbano essere libere.`,
export_feed_explanation: `Per seguire gli aggiornamenti da computer o smartphone senza la necessità di aprire periodicamente il sito, il metodo consigliato è quello dei Feed RSS.</p>
<p>Con i feed rss utilizzi un'apposita applicazione per ricevere aggiornamenti dai siti che più ti interessano. È un buon metodo per seguire anche molti siti in modo molto rapido, senza necessità di creare un account o altre complicazioni.</p>
<p>Se hai Android, ti consigliamo <a href="https://play.google.com/store/apps/details?id=net.frju.flym">Flym</a> o Feeder<br />
Per iPhone/iPad puoi usare <a href="https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8">Feed4U</a><br />
Per il computer fisso/portatile consigliamo Feedbro, da installare all'interno <a href="https://addons.mozilla.org/en-GB/firefox/addon/feedbroreader/">di Firefox </a>o <a href="https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa">di Chrome</a> e compatibile con tutti i principali sistemi operativi.</p>
Aggiungendo il link sopra, rimarrai aggiornata sui seguenti eventi:`,
Poster: 'Locandina',
Settings: 'Impostazioni',
Search: 'Cerca',
Send: 'Invia',
Register: 'Registrati',
Logout: 'Esci',
Login: 'Entra',
SignIn: 'Registrati',
Cancel: 'Annulla',
Next: 'Continua',
Prev: 'Indietro',
Username: 'Utente',
Description: 'Descrizione',
Deactivate: 'Disattiva',
Activate: 'Attiva',
'Remove Admin': 'Rimuovi Admin',
Users: 'Utenti',
Places: 'Luoghi',
Tags: 'Etichette',
Remove: 'Elimina',
Edit: 'Modifica',
Admin: 'Amministra',
Today: 'Oggi',
'Edit event': 'Modifica evento',
'New event': 'Nuovo evento',
registration_complete: 'Controlla la tua posta (anche la cartella spam)',
registration_email: `Ciao, la tua registrazione sarà confermata nei prossimi giorni. Riceverai una conferma non temere.`,
register_explanation: `I movimenti hanno bisogno di organizzarsi e autofinanziarsi. <br/>Questo è un dono per voi, non possiamo più vedervi usare le piattaforme del capitalismo. Solo eventi non commerciali e ovviamente antifascisti, antisessisti, antirazzisti.
<br/>Prima di poter pubblicare <strong>dobbiamo approvare l'account</strong>, considera che <strong>dietro questo sito ci sono delle persone</strong> di
carne e sangue, scrivici quindi due righe per farci capire che eventi vorresti pubblicare.`
}
module.exports = it
+68
View File
@@ -0,0 +1,68 @@
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import BootstrapVue from 'bootstrap-vue'
import VCalendar from 'v-calendar'
import 'vue-awesome/icons'
import Icon from 'vue-awesome/components/Icon'
import Typeahead from '@/components/Typeahead'
import VueClipboard from 'vue-clipboard2'
import 'v-calendar/lib/v-calendar.min.css'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import itElementLocale from 'element-ui/lib/locale/lang/it'
import enElementLocale from 'element-ui/lib/locale/lang/en'
import App from './App.vue'
import router from './router'
import store from './store'
import itLocale from '@/locale/it'
import enLocale from '@/locale/en'
// Use v-calendar, v-date-picker & v-popover components
Vue.use(VCalendar, {
firstDayOfWeek: 2
})
Vue.use(BootstrapVue)
Vue.use(VueI18n)
Vue.use(VueClipboard)
Vue.component('typeahead', Typeahead)
Vue.component('v-icon', Icon)
const messages = {
en: {
...enElementLocale,
...enLocale
},
it: {
...itElementLocale,
...itLocale
}
}
// Create VueI18n instance with options
const i18n = new VueI18n({
locale: 'it', // set locale
messages // set locale messages
})
Vue.use(ElementUI, { i18n: (key, value) => i18n.t(key, value) })
Vue.config.productionTip = false
Vue.config.lang = 'it'
Vue.config.devtools = true
Vue.config.silent = false
new Vue({
i18n,
router,
store,
render: h => h(App)
}).$mount('#app')
+51
View File
@@ -0,0 +1,51 @@
import Vue from 'vue'
import Router from 'vue-router'
import Settings from './components/Settings'
import newEvent from './components/newEvent'
import EventDetail from './components/EventDetail'
import Login from './components/Login'
import Register from './components/Register'
import Export from './components/Export'
import Admin from './components/Admin'
Vue.use(Router)
export default new Router({
mode: 'history',
routes: [
{
path: '/admin',
components: { modal: Admin }
},
{
path: '/register',
components: { modal: Register }
},
{
path: '/login',
components: { modal: Login }
},
{
path: '/new_event',
components: { modal: newEvent }
},
{
path: '/settings',
components: { modal: Settings }
},
{
path: '/event/:id',
components: { modal: EventDetail }
},
{
path: '/edit/:id',
components: { modal: newEvent },
props: { edit: true }
},
{
path: '/export/:type',
components: { modal: Export }
}
]
})
+119
View File
@@ -0,0 +1,119 @@
import Vue from 'vue'
import Vuex from 'vuex'
import VuexPersistence from 'vuex-persist'
import api from './api'
Vue.use(Vuex)
const vuexLocal = new VuexPersistence({
storage: window.localStorage,
reducer: state => ({ logged: state.logged, user: state.user, token: state.token })
})
export default new Vuex.Store({
plugins: [vuexLocal.plugin],
getters: {
token: state => state.token
},
state: {
logged: false,
user: {},
token: '',
events: [],
tags: [],
places: [],
//
filters: {
tags: [],
places: [],
hidePast: false
}
},
mutations: {
logout (state) {
state.logged = false
state.token = ''
state.user = {}
},
login (state, user) {
state.logged = true
state.user = user.user
state.token = user.token
},
setEvents (state, events) {
state.events = events
},
addEvent (state, event) {
state.events.push(event)
},
updateEvent (state, event) {
state.events = state.events.map(e => {
if (e.id !== event.id) return e
return event
})
},
delEvent (state, eventId) {
state.events = state.events.filter(ev => ev.id !== eventId)
},
update (state, { tags, places }) {
state.tags = tags
state.places = places
},
// search
addSearchTag (state, tag) {
if (!state.filters.tags.find(t => t === tag.tag)) {
state.filters.tags.push(tag.tag)
}
},
setSearchTags (state, tags) {
state.filters.tags = tags
},
addSearchPlace (state, place) {
if (state.filters.places.find(p => p.name === place.name)) {
state.filters.places.push(place)
}
},
setSearchPlaces (state, places) {
state.filters.places = places
}
},
actions: {
async updateEvents ({ commit }, date) {
const events = await api.getAllEvents(date.month - 1, date.year)
commit('setEvents', events)
},
async updateMeta ({ commit }) {
const { tags, places } = await api.getMeta()
commit('update', { tags, places })
},
async addEvent ({ commit }, formData) {
const event = await api.addEvent(formData)
commit('addEvent', event)
},
async updateEvent ({ commit }, formData) {
const event = await api.updateEvent(formData)
commit('updateEvent', event)
},
delEvent ({ commit }, eventId) {
commit('delEvent', eventId)
},
login ({ commit }, user) {
commit('login', user)
},
logout ({ commit }) {
commit('logout')
},
// search
addSearchTag ({ commit }, tag) {
commit('addSearchTag', tag)
},
setSearchTags ({ commit }, tags) {
commit('setSearchTags', tags)
},
addSearchPlace ({ commit }, place) {
commit('addSearchPlace', place)
},
setSearchPlaces ({ commit }, places) {
commit('setSearchPlaces', places)
}
}
})
+9
View File
@@ -0,0 +1,9 @@
module.exports = {
devServer: {
disableHostCheck: true
},
transpileDependencies: [
/\bvue-awesome\b/,
'vuex-persist'
]
}
+10875
View File
File diff suppressed because it is too large Load Diff
-35
View File
@@ -1,35 +0,0 @@
const path = require('path')
module.exports = {
// environment
env: 'development',
locale: 'it',
title: 'Gancio',
description: 'Un calendario dei movimenti piemontesi',
// base url
baseurl: 'http://localhost:8080',
apiurl: 'http://localhost:9000/api',
// db configuration
db: {
'storage': path.join(__dirname, '/../db.sqlite'),
'dialect': 'sqlite'
},
admin: 'lesion@autistici.org',
// email configuration
smtp: {
host: 'mail.example.com',
secure: true,
auth: {
user: 'user@example.com',
pass: 'password'
}
},
// jwt secret
secret: 'nonosecretsuper'
}
+27
View File
@@ -0,0 +1,27 @@
{
"env": "development",
"locale": "it",
"title": "Gancio",
"description": "Un calendario dei movimenti piemontesi",
"baseurl": "http://localhost:8080",
"apiurl": "http://localhost:9000/api",
"db": {
"storage": "./db.sqlite",
"dialect": "sqlite"
},
"admin": "lesion@autistici.org",
"smtp": {
"host": "mail.example.org",
"secure": true,
"auth": {
"user": "events@example.org",
"pass": ""
}
},
"secret": "nonosecretsuper"
}
-30
View File
@@ -1,30 +0,0 @@
module.exports = {
// environment
env: 'production',
locale: 'en',
title: 'Put here your site name',
description: 'A calendar for radical communities',
// base url
baseurl: 'https://example.com',
apiurl: 'https://example.com/api',
// db configuration
db: {
},
admin: 'admin@example.com',
// email configuration
smtp: {
host: 'mail.example.com',
secure: true,
auth: {
user: 'admin@example.com',
pass: ''
}
},
// jwt secret
secret: 'randomstringhere'
}
+25
View File
@@ -0,0 +1,25 @@
{
"env": "production",
"locale": "en",
"title": "Put here your site name",
"description": "A calendar for radical communities",
"baseurl": "https://example.com",
"apiurl": "https://example.com/api",
"db": {
},
"admin": "admin@example.com",
"smtp": {
"host": "mail.example.com",
"secure":"true",
"auth": {
"user": "admin@example.com",
"pass": ""
}
},
"secret": "randomstringhere"
}
-1
View File
@@ -4,7 +4,6 @@ const bodyParser = require('body-parser')
const api = require('./app/api')
const cors = require('cors')
const path = require('path')
const db = require('./app/db')
const port = process.env.PORT || 8080
app.use(bodyParser.urlencoded({ extended: false }))