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

139
client/src/App.vue Normal file
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
client/src/api.js Normal file
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')
}

View File

@@ -0,0 +1,5 @@
/* html, body {
max-height: 100%;
overflow: hidden;
} */

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>

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>

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>

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>

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>

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>

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>

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>

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>

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>

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>

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 }

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
client/src/filters.js Normal file
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
client/src/locale/en.js Normal file
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
client/src/locale/it.js Normal file
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
client/src/main.js Normal file
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
client/src/router.js Normal file
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
client/src/store.js Normal file
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)
}
}
})