front end, config
This commit is contained in:
67
client/src/components/Admin.vue
Normal file
67
client/src/components/Admin.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template lang="pug">
|
||||
b-modal(hide-footer hide-header
|
||||
@hide='$router.go(-1)' size='lg' :visible='true')
|
||||
h4.text-center Admin
|
||||
b-tabs(pills)
|
||||
b-tab
|
||||
template(slot='title')
|
||||
v-icon(name='users')
|
||||
span {{$t('Users')}}
|
||||
b-table(:items='users' :fields='userFields' striped hover)
|
||||
template(slot='action' slot-scope='data')
|
||||
b-button.mr-1(:variant='data.item.is_active?"warning":"success"' @click='toggle(data.item)') {{data.item.is_active?$t('Deactivate'):$t('Activate')}}
|
||||
b-button(:variant='data.item.is_admin?"danger":"warning"' @click='toggleAdmin(data.item)') {{data.item.is_admin?$t('Remove Admin'):$t('Admin')}}
|
||||
b-tab
|
||||
template(slot='title')
|
||||
v-icon(name='map-marker-alt')
|
||||
span {{$t('Places')}}
|
||||
b-table(:items='places' :fields='placeFields' striped hover)
|
||||
b-tab
|
||||
template(slot='title')
|
||||
v-icon(name='tag')
|
||||
span {{$t('Tags')}}
|
||||
b-table(:items='tags' :fields='tagFields' striped hover)
|
||||
template(slot='tag' slot-scope='data')
|
||||
b-badge(:style='{backgroundColor: data.item.color}') {{data.item.tag}}
|
||||
template(slot='color' slot-scope='data')
|
||||
el-color-picker(v-model='data.item.color' @change='updateColor(data.item)')
|
||||
b-tab
|
||||
template(slot='title')
|
||||
v-icon(name='tools')
|
||||
span {{$t('Settings')}}
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import api from '@/api'
|
||||
|
||||
export default {
|
||||
name: 'Admin',
|
||||
data () {
|
||||
return {
|
||||
users: [],
|
||||
userFields: ['email', 'action'],
|
||||
placeFields: ['name', 'address'],
|
||||
tagFields: ['tag', 'color'],
|
||||
description: '',
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
this.users = await api.getUsers()
|
||||
},
|
||||
computed: mapState(['tags', 'places']),
|
||||
methods: {
|
||||
async toggle(user) {
|
||||
user.is_active = !user.is_active
|
||||
const newuser = await api.updateUser(user)
|
||||
},
|
||||
async toggleAdmin(user) {
|
||||
user.is_admin = !user.is_admin
|
||||
const newuser = await api.updateUser(user)
|
||||
},
|
||||
async updateColor(tag) {
|
||||
const newTag = await api.updateTag(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
82
client/src/components/Calendar.vue
Normal file
82
client/src/components/Calendar.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template lang="pug">
|
||||
v-calendar#calendar.card(
|
||||
:attributes='attributes'
|
||||
:from-page.sync='page'
|
||||
is-expanded is-inline)
|
||||
div(slot='popover', slot-scope='{ customData }')
|
||||
router-link(:to="`/event/${customData.id}`") {{customData.start_datetime|hour}} - {{customData.title}} @{{customData.place.name}}
|
||||
</template>
|
||||
<script>
|
||||
import { mapState, mapActions } from 'vuex'
|
||||
import filters from '@/filters'
|
||||
import moment from 'moment'
|
||||
|
||||
export default {
|
||||
name: 'Calendar',
|
||||
filters,
|
||||
data () {
|
||||
const month = moment().month()+1
|
||||
const year = moment().year()
|
||||
return {
|
||||
page: { month, year},
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.updateEvents(this.page)
|
||||
},
|
||||
watch: {
|
||||
page () {
|
||||
this.updateEvents(this.page)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateEvents']),
|
||||
eventToAttribute(event) {
|
||||
let e = {
|
||||
key: event.id,
|
||||
customData: event,
|
||||
order: event.start_datetime,
|
||||
popover: {
|
||||
slot: 'popover',
|
||||
visibility: 'hover'
|
||||
}
|
||||
}
|
||||
|
||||
const color = event.tags.length && event.tags[0].color ? event.tags[0].color : 'rgba(200,200,200,0.5)'
|
||||
if (event.multidate) {
|
||||
e.dates = {
|
||||
start: event.start_datetime, end: event.end_datetime
|
||||
}
|
||||
e.highlight = { backgroundColor: color,
|
||||
borderColor: 'transparent',
|
||||
borderWidth: '4px' }
|
||||
} else {
|
||||
e.dates = event.start_datetime
|
||||
e.dot = { backgroundColor: color, borderColor: color, borderWidth: '3px' }
|
||||
}
|
||||
return e
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['events', 'tags']),
|
||||
attributes () {
|
||||
return [
|
||||
{ key: 'todaly', dates: new Date(),
|
||||
highlight: {
|
||||
backgroundColor: '#aaffaa'
|
||||
},
|
||||
popover: {label: this.$t('Today')}
|
||||
},
|
||||
...this.events.map(this.eventToAttribute)
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#calendar {
|
||||
margin-bottom: 0em;
|
||||
margin-top: 0.3em;
|
||||
}
|
||||
</style>
|
||||
60
client/src/components/Event.vue
Normal file
60
client/src/components/Event.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template lang="pug">
|
||||
b-card(bg-variant='dark' text-variant='white'
|
||||
@click='$router.push("/event/" + event.id)'
|
||||
:img-src='imgPath')
|
||||
h4 {{event.title}}
|
||||
div <v-icon name='clock'/> {{event.start_datetime|datetime}}
|
||||
span(v-b-popover.hover="event.place && event.place.address || ''") <v-icon name='map-marker-alt'/> {{event.place.name}}
|
||||
br
|
||||
b-badge(:style='{backgroundColor: tag.color}' v-for='tag in event.tags' href='#'
|
||||
@click.stop='addSearchTag(tag)') {{tag.tag}}
|
||||
</template>
|
||||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import api from '@/api'
|
||||
import filters from '@/filters'
|
||||
import config from '../../config'
|
||||
|
||||
export default {
|
||||
props: ['event'],
|
||||
computed: {
|
||||
...mapState(['user']),
|
||||
imgPath () {
|
||||
return this.event.image_path && config.apiurl + '/../' + this.event.image_path
|
||||
},
|
||||
mine () {
|
||||
return this.event.userId === this.user.id
|
||||
}
|
||||
},
|
||||
filters,
|
||||
methods: {
|
||||
...mapActions(['delEvent', 'addSearchTag']),
|
||||
async remove () {
|
||||
await api.delEvent(this.event.id)
|
||||
this.delEvent(this.event.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
/* .card::before {
|
||||
border-top: 4px solid black;
|
||||
content: ''
|
||||
} */
|
||||
|
||||
.card-columns .card {
|
||||
margin-top: 0.3em;
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
|
||||
.card-img {
|
||||
max-height: 180px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.badge {
|
||||
margin-left: 0.1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
91
client/src/components/EventDetail.vue
Normal file
91
client/src/components/EventDetail.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template lang="pug">
|
||||
b-modal#eventDetail(hide-footer hide-header
|
||||
@hide='$router.go(-1)' size='lg' :visible='true')
|
||||
|
||||
b-card(bg-variant='dark' href='#' text-variant='white'
|
||||
no-body, :img-src='imgPath')
|
||||
b-card-header
|
||||
h3 {{event.title}}
|
||||
v-icon(name='clock')
|
||||
span {{event.start_datetime|datetime}}
|
||||
br
|
||||
v-icon(name='map-marker-alt')
|
||||
span {{event.place.name}} - {{event.place.address}}
|
||||
br
|
||||
b-card-footer(v-if='event.description || event.tags') {{event.description}}
|
||||
br
|
||||
b-badge(:style='{backgroundColor: tag.color}' v-for='tag in event.tags') {{tag.tag}}
|
||||
b-navbar(v-if='mine' type="dark" variant="dark" toggleable='lg')
|
||||
b-navbar-nav.ml-auto
|
||||
b-nav-item(@click.prevent='remove') <v-icon color='red' name='times'/> {{$t('Remove')}}
|
||||
b-nav-item(:to='"/edit/"+event.id') <v-icon color='orange' name='edit'/> {{$t('Edit')}}
|
||||
//- b-card-footer.text-right
|
||||
//- span.mr-3 {{event.comments.length}} <v-icon name='comments'/>
|
||||
//- a(href='#', @click='remove')
|
||||
v-icon(color='orange' name='times')
|
||||
//- b-card-footer(v-for='comment in event.comments')
|
||||
strong {{comment.author}}
|
||||
div(v-html='comment.text')
|
||||
</template>
|
||||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import api from '@/api'
|
||||
import filters from '@/filters'
|
||||
import config from '../../config'
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapState(['user']),
|
||||
imgPath () {
|
||||
return this.event.image_path && config.apiurl + '/../' + this.event.image_path
|
||||
},
|
||||
mine () {
|
||||
return this.event.userId === this.user.id || this.user.is_admin
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
event: { comments: [], place: {}},
|
||||
id: null,
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.id = this.$route.params.id
|
||||
this.load()
|
||||
},
|
||||
filters: filters,
|
||||
methods: {
|
||||
...mapActions(['delEvent']),
|
||||
async load () {
|
||||
const event = await api.getEvent(this.id)
|
||||
this.event = event
|
||||
},
|
||||
async remove () {
|
||||
await api.delEvent(this.event.id)
|
||||
this.delEvent(this.event.id)
|
||||
this.$router.go(-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#eventDetail .modal-body {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
/* .card::before {
|
||||
border-top: 4px solid black;
|
||||
content: ''
|
||||
} */
|
||||
#eventDetail .card {
|
||||
margin-left: -5px;
|
||||
}
|
||||
/* #eventDetail .card-img {
|
||||
max-height: 150px;
|
||||
object-fit: cover;
|
||||
} */
|
||||
#eventDetail .badge {
|
||||
margin-left: 0.1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
60
client/src/components/Export.vue
Normal file
60
client/src/components/Export.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template lang="pug">
|
||||
b-modal(hide-footer hide-header
|
||||
@hide='$router.go(-1)' size='lg' :visible='true' v-if='type')
|
||||
h3.text-center Export {{type}}
|
||||
b-input-group.mb-2(v-if='showLink')
|
||||
b-form-input( v-model='link' autocomplete='off')
|
||||
b-input-group-append
|
||||
b-button(variant="success" v-clipboard:copy="link") <v-icon name='clipboard'/> Copy
|
||||
p {{$t('export_intro')}}
|
||||
p(v-html='$t(`export_${type}_explanation`)')
|
||||
li(v-if='filters.tags.length') {{$t('Tags')}} ->
|
||||
b-badge.ml-1(v-for='tag in filters.tags') {{tag}}
|
||||
li(v-if='filters.places.length') {{$t('Places')}}
|
||||
b-badge.ml-1(v-for='place in filters.places') {{place}}
|
||||
b-form(v-if="type==='email'")
|
||||
el-switch(v-model='mail.sendOnInsert' :active-text="$t('notify_on_insert')")
|
||||
br
|
||||
el-switch(v-model='mail.reminder' :active-text="$t('send_reminder')")
|
||||
b-form-input.mt-1(v-model='mail.mail' :placeholder="$t('Insert your address')")
|
||||
b-button.mt-1.float-right(variant='success' @click='activate_email') {{$t('Send')}}
|
||||
</template>
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import config from '../../config'
|
||||
import path from 'path'
|
||||
|
||||
export default {
|
||||
name: 'Export',
|
||||
data () {
|
||||
return {
|
||||
type: '',
|
||||
link: '',
|
||||
mail: {}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.type = this.$route.params.type
|
||||
this.link = this.loadLink()
|
||||
if (this.type === 'email' && this.logged) {
|
||||
this.mail.mail = this.user.email
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
activate_email () {
|
||||
this.$router.go(-1)
|
||||
},
|
||||
loadLink () {
|
||||
const filters = this.filters.tags.join(',')
|
||||
return `${config.apiurl}/export/${this.type}/${filters}`
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['filters', 'user', 'logged']),
|
||||
showLink () {
|
||||
return (['feed', 'ics'].indexOf(this.type)>-1)
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
81
client/src/components/Home.vue
Normal file
81
client/src/components/Home.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template lang="pug">
|
||||
b-container
|
||||
b-card-group(columns)
|
||||
Calendar
|
||||
//- transition-group(name="list" tag="div")
|
||||
Event.item(v-for='event in filteredEvents'
|
||||
:key='event.id'
|
||||
:event='event')
|
||||
</template>
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import filters from '@/filters.js'
|
||||
import Event from '@/components/Event'
|
||||
import Calendar from '@/components/Calendar'
|
||||
import {intersection} from 'lodash'
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: { Event, Calendar },
|
||||
computed: {
|
||||
...mapState(['events', 'filters']),
|
||||
filteredEvents () {
|
||||
if (!this.filters.tags.length && !this.filters.places.length) return this.events
|
||||
return this.events.filter(e => {
|
||||
if (this.filters.tags.length) {
|
||||
const m = intersection(e.tags.map(t => t.tag), this.filters.tags)
|
||||
if (m.length>0) return true
|
||||
}
|
||||
if (this.filters.places.length) {
|
||||
if (this.filters.places.find(p => p === e.place.name))
|
||||
return true
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
|
||||
.card-columns {
|
||||
column-count: 1;
|
||||
column-gap: 0.3em;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.container {
|
||||
max-width: none;
|
||||
}
|
||||
.card-columns {
|
||||
column-count: 2;
|
||||
column-gap: 0.3em;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 950px) {
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
}
|
||||
.card-columns {
|
||||
column-count: 3;
|
||||
column-gap: 0.3em;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
transition: all .2s;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
.list-enter, .list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
.list-leave-active {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
width: 0px;
|
||||
left: 0px;
|
||||
height: 0px;
|
||||
z-index: -10;
|
||||
}
|
||||
</style>
|
||||
47
client/src/components/Login.vue
Normal file
47
client/src/components/Login.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template lang='pug'>
|
||||
b-modal(hide-header hide-footer @shown="$refs.email.focus()"
|
||||
@hide='$router.go(-1)' :visible='true')
|
||||
h4.text-center.center {{$t('Login')}}
|
||||
b-form
|
||||
//- p.text-muted Sign In to your account
|
||||
b-input-group.mb-1
|
||||
b-input-group-prepend
|
||||
b-input-group-text
|
||||
v-icon(name="user")
|
||||
b-form-input(ref='email' v-model="email" type="text" class="form-control" placeholder="E-mail" autocomplete="email")
|
||||
b-input-group.mb-3
|
||||
b-input-group-prepend
|
||||
b-input-group-text
|
||||
v-icon(name="lock")
|
||||
b-form-input(v-model="password" type="password" class="form-control" placeholder="Password" autocomplete="current-password")
|
||||
b-button.float-right(variant="success" @click='submit') Login
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/api'
|
||||
import { mapActions } from 'vuex'
|
||||
import { log } from 'util';
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
data () {
|
||||
return {
|
||||
password: '',
|
||||
email: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['login']),
|
||||
async submit (e) {
|
||||
e.preventDefault()
|
||||
const user = await api.login(this.email, this.password)
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
this.login(user)
|
||||
this.email = this.password = ''
|
||||
this.$router.go(-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
58
client/src/components/Register.vue
Normal file
58
client/src/components/Register.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template lang='pug'>
|
||||
b-modal(hide-header hide-footer
|
||||
@hide='$router.go(-1)' :visible='true' @shown='$refs.email.focus()')
|
||||
h4.text-center.center {{$t('Register')}}
|
||||
b-form
|
||||
p.text-muted(v-html="$t('register_explanation')")
|
||||
b-input-group.mb-1
|
||||
b-input-group-prepend
|
||||
b-input-group-text @
|
||||
b-form-input(ref='email' v-model='user.email' type="text" class="form-control" placeholder="Email" autocomplete="email" )
|
||||
|
||||
b-input-group.mb-1
|
||||
b-input-group-prepend
|
||||
b-input-group-text
|
||||
v-icon(name='lock')
|
||||
b-form-input(v-model='user.password' type="password" class="form-control" placeholder="Password")
|
||||
|
||||
b-input-group.mb-1
|
||||
b-input-group-prepend
|
||||
b-input-group-text
|
||||
v-icon(name='envelope-open-text')
|
||||
b-form-textarea(v-model='user.description' type="text" rows='3' class="form-control" :placeholder="$t('Description')")
|
||||
|
||||
|
||||
|
||||
b-button.float-right(variant="success" @click='register') {{$t('Send')}}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/api'
|
||||
import { mapActions } from 'vuex';
|
||||
export default {
|
||||
name: 'Register',
|
||||
data () {
|
||||
return {
|
||||
error: {},
|
||||
user: { }
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['login']),
|
||||
async register () {
|
||||
try {
|
||||
const user = await api.register(this.user)
|
||||
// this.login(user)
|
||||
// this.user = { name: '' }
|
||||
this.$router.go(-1)
|
||||
this.$message({
|
||||
message: this.$t('registration_complete'),
|
||||
type: 'success'
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
44
client/src/components/Settings.vue
Normal file
44
client/src/components/Settings.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template lang="pug">
|
||||
b-modal(hide-header hide-footer @hide='$router.push("/")' :visible='true')
|
||||
h4.text-center {{$t('Settings')}}
|
||||
b-form
|
||||
b-input-group.mt-1(prepend='Email')
|
||||
b-form-input(v-model="user.email")
|
||||
//- b-form-checkbox(v-model="tmpUser.user.autoboost") Autoboost
|
||||
b-input-group.mt-1(prepend='Mastodon instance')
|
||||
b-form-input(v-model="mastodon_instance")
|
||||
b-input-group-append
|
||||
b-button(@click='associate', variant='primary') Associate
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import { mapState, mapActions } from 'vuex'
|
||||
import api from '@/api'
|
||||
export default {
|
||||
props: ['code'],
|
||||
data () {
|
||||
return {
|
||||
mastodon_instance: '',
|
||||
user: {}
|
||||
}
|
||||
},
|
||||
computed: mapState(['oauth']),
|
||||
async mounted () {
|
||||
const code = this.$route.query.code
|
||||
if (code) {
|
||||
const res = await api.setCode({code})
|
||||
}
|
||||
|
||||
const user = await api.getUser()
|
||||
this.user = user
|
||||
this.mastodon_instance = user.instance
|
||||
},
|
||||
methods: {
|
||||
async associate () {
|
||||
const url = await api.getAuthURL({instance: this.mastodon_instance})
|
||||
setTimeout( () => window.location.href=url, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
17
client/src/components/Timeline.vue
Normal file
17
client/src/components/Timeline.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template lang="pug">
|
||||
b-card.column.pl-1(bg-variant='dark' text-variant='white' no-body)
|
||||
b-card-header
|
||||
strong Public events
|
||||
b-btn.float-right(v-if='logged' variant='success' size='sm' to='/newEvent') <v-icon name="plus"/> Add Event
|
||||
event(v-for='event in events', :event='event' :key='event.id')
|
||||
</template>
|
||||
<script>
|
||||
import api from '@/api'
|
||||
import event from './Event'
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {event},
|
||||
computed: mapState(['events', 'logged'])
|
||||
}
|
||||
</script>
|
||||
240
client/src/components/Typeahead.vue
Normal file
240
client/src/components/Typeahead.vue
Normal file
@@ -0,0 +1,240 @@
|
||||
<template lang='pug'>
|
||||
div(style="position: relative")
|
||||
|
||||
b-input-group
|
||||
input.form-control(type="search"
|
||||
ref="input"
|
||||
:placeholder="placeholder"
|
||||
v-model="search"
|
||||
@input="update"
|
||||
autocomplete="off"
|
||||
@keydown.backspace="backspace"
|
||||
@keydown.up.prevent="up"
|
||||
@keydown.down.prevent="down"
|
||||
@keydown.enter="hit"
|
||||
@keydown.esc="reset(true)"
|
||||
@blur="focus = false"
|
||||
@focus="focus = true")
|
||||
|
||||
div
|
||||
b-badge.mr-1(@click="removeSelected(sel)"
|
||||
v-for="sel in selectedLabel"
|
||||
:key="sel") {{sel}}
|
||||
|
||||
b-list-group.groupMenu(v-show='showDropdown')
|
||||
b-list-group-item(:key="$index" v-for="(item, $index) in matched"
|
||||
href='#'
|
||||
:class="{'active': isActive($index)}"
|
||||
@mousedown.prevent="hit"
|
||||
@mousemove="setActive($index)")
|
||||
slot(:name="templateName") {{textField ? item[textField] : item}}
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
twoWay : true,
|
||||
type: [String, Array, Set],
|
||||
default: ''
|
||||
},
|
||||
data: {
|
||||
type: Array
|
||||
},
|
||||
template: {
|
||||
type: String
|
||||
},
|
||||
templateName: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
valueField: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
textField: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
showClear: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
matchCase: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
matchStart: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
onHit: {
|
||||
type: Function,
|
||||
default () {
|
||||
this.reset()
|
||||
}
|
||||
},
|
||||
placeholder: {
|
||||
type: String
|
||||
},
|
||||
updateOnMatchOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
maxMatch: {
|
||||
type: Number,
|
||||
default: 4
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
focus: false,
|
||||
noResults: true,
|
||||
current: 0,
|
||||
search: '',
|
||||
selected: [],
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(newValue) {
|
||||
if (!newValue) {
|
||||
this.search = '';
|
||||
if (this.multiple) this.$emit('input', []) // this.selected = [];
|
||||
} else {
|
||||
if (!this.multiple) {
|
||||
this.search = newValue
|
||||
} else {
|
||||
this.selected = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showDropdown () {
|
||||
return this.focus
|
||||
},
|
||||
selectedValues () {
|
||||
return this.selected.map(s => this.valueField ? (s[this.valueField] || s) : s);
|
||||
},
|
||||
selectedLabel () {
|
||||
return this.selected.map(s => this.textField ? s[this.textField] || s: s);
|
||||
},
|
||||
matched () {
|
||||
if (this.data) {
|
||||
return this.data.filter(value => {
|
||||
if(this.textField) value = value[this.textField];
|
||||
if (this.multiple && this.selectedLabel.includes(value)) return false;
|
||||
value = this.matchCase ? value : value.toLowerCase()
|
||||
const query = this.matchCase ? this.search : this.search.toLowerCase()
|
||||
return this.matchStart ? value.indexOf(query) === 0 : value.indexOf(query) !== -1
|
||||
}).slice(0, this.maxMatch)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update (e) {
|
||||
if (this.multiple && e.data === ',') {
|
||||
this.search = this.search.substr(0, this.search.length-1)
|
||||
this.hit(e)
|
||||
return
|
||||
}
|
||||
if (!this.updateOnMatchOnly && !this.multiple) this.$emit('input', this.search);
|
||||
this.focus = true;
|
||||
if (!this.matched.length) {
|
||||
this.focus = false;
|
||||
this.current = 0
|
||||
return;
|
||||
}
|
||||
|
||||
// current selected item has to be in the match
|
||||
if (this.matched.length <= this.current) {
|
||||
this.current = this.matched.length-1;
|
||||
}
|
||||
},
|
||||
backspace () {
|
||||
if (this.search) return
|
||||
this.selected.splice(-1, 1)
|
||||
this.$emit('input', this.selected.length ? this.selectedValues : '');
|
||||
},
|
||||
reset (esc=false) {
|
||||
this.search = '';
|
||||
this.current = 0;
|
||||
this.$refs.input.focus();
|
||||
if (esc) {
|
||||
this.focus = false
|
||||
} else {
|
||||
this.selected = [];
|
||||
this.$emit('input', '');
|
||||
}
|
||||
},
|
||||
setActive (index) {
|
||||
this.current = index
|
||||
},
|
||||
isActive (index) {
|
||||
return this.current === index
|
||||
},
|
||||
|
||||
removeSelected (label) {
|
||||
this.selected = this.selected.filter( s => (this.textField ? s[this.textField] || s: s) !== label);
|
||||
this.$emit('input', this.selected.length ? this.selectedValues : []);
|
||||
},
|
||||
|
||||
// click or enter on curren item
|
||||
hit (e) {
|
||||
e.preventDefault();
|
||||
let code = '';
|
||||
let item = '';
|
||||
|
||||
if (this.matched.length !== 0 && this.focus) {
|
||||
item = this.matched[this.current];
|
||||
code = this.textField ? item[this.textField] : item;
|
||||
// code = this.valueField ? item[this.valueField] : item;
|
||||
} else {
|
||||
code = this.search;
|
||||
}
|
||||
if (this.multiple) {
|
||||
if (code) {
|
||||
this.selected.push(code);
|
||||
this.search = '';
|
||||
this.$emit('input', this.selected);
|
||||
this.focus = false;
|
||||
// this.update();
|
||||
}
|
||||
} else {
|
||||
this.$emit('input', code);
|
||||
this.current = 0;
|
||||
this.focus = false;
|
||||
this.search = code
|
||||
}
|
||||
this.$emit('enter')
|
||||
},
|
||||
|
||||
// manage up/down arrow key
|
||||
up () {
|
||||
if (this.current > 0) this.current--
|
||||
},
|
||||
down () {
|
||||
if (this.current < this.matched.length - 1) this.current++
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.groupMenu {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.badge {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
9
client/src/components/index.js
Normal file
9
client/src/components/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import Register from '@/components/Register'
|
||||
import Login from '@/components/Login'
|
||||
import Settings from '@/components/Settings'
|
||||
import newEvent from '@/components/newEvent'
|
||||
import eventDetail from '@/components/EventDetail'
|
||||
import Home from '@/components/Home'
|
||||
import Event from '@/components/event'
|
||||
|
||||
module.exports = { Home, eventDetail, newEvent, Settings, Login, Register, Event }
|
||||
206
client/src/components/newEvent.vue
Normal file
206
client/src/components/newEvent.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<template lang="pug">
|
||||
b-modal(hide-header hide-footer no-close-on-backdrop
|
||||
@hide='$router.go(-1)' no-close-on-esc size='lg' :visible='true')
|
||||
h4.text-center.center {{edit?$t('Edit event'):$t('New event')}}
|
||||
b-tabs#tabss(pills v-model='activeTab')
|
||||
b-form
|
||||
b-tab
|
||||
template(slot='title')
|
||||
v-icon(name='map-marker-alt')
|
||||
span {{$t('Where')}}
|
||||
b-card-body
|
||||
span.text-muted {{$t('where_explanation')}}
|
||||
typeahead.mb-3(v-model='event.place.name' :data='places_name' @enter='placeChoosed')
|
||||
span.text-muted {{$t('address_explanation')}}
|
||||
b-form-input(ref='address' v-model='event.place.address' @keydown.native.enter='next')
|
||||
b-tab
|
||||
template(slot='title')
|
||||
v-icon(name='clock')
|
||||
span {{$t('When')}}
|
||||
b-card-body
|
||||
el-switch.float-right(v-model='event.multidate' :active-text="$t('multidate_explanation')")
|
||||
span.text-muted {{event.multidate ? $t('dates_explanation') : $t('date_explanation')}}
|
||||
v-date-picker.mb-3(:mode='event.multidate ? "range" : "single"' v-model='date' is-inline
|
||||
is-expanded :min-date='new Date()' @input='date ? $refs.time_start.focus() : false')
|
||||
b-row
|
||||
b-col
|
||||
label.text-muted {{$t('time_start_explanation')}}
|
||||
el-time-select(ref='time_start'
|
||||
v-model="time.start"
|
||||
:picker-options="{ start: '00:00', step: '00:30', end: '24:00'}")
|
||||
b-col.text-right
|
||||
label.text-muted {{$t('time_end_explanation')}}
|
||||
el-time-select(
|
||||
v-model='time.end'
|
||||
:picker-options="{start: `${event.multidate?'00:00':time.start}`, step: '00:30', end: '24:00'}"
|
||||
)
|
||||
|
||||
b-tab
|
||||
template(slot='title')
|
||||
v-icon(name='file-alt')
|
||||
span {{$t('What')}}
|
||||
b-card-body
|
||||
span.text-muted {{$t('what_explanation')}}
|
||||
b-form-input.mb-3(v-model.trim='event.title' autocomplete='off')
|
||||
span.text-muted {{$t('description_explanation')}}
|
||||
b-form-textarea.mb-3(v-model='event.description' :rows='3')
|
||||
span.text-muted {{$t('tag_explanation')}}
|
||||
typeahead(v-model="event.tags" :data='tags' multiple)
|
||||
b-tab
|
||||
template(slot='title')
|
||||
v-icon(name='image')
|
||||
span {{$t('Media')}}
|
||||
b-card-body
|
||||
span.text-muted {{$t('media_explanation')}}
|
||||
b-form-file(v-model='event.image', :placeholder='$t("Poster")' accept='image/*')
|
||||
b-button(v-if='activeTab==0' variant='danger' @click='$router.go(-1)') {{$t('Cancel')}}
|
||||
b-button.float-left(v-else variant='danger' @click='prev') {{$t('Prev')}}
|
||||
b-button.float-right(v-if='activeTab<3' variant='success' @click='next' :disabled='!couldProceed') {{$t('Next')}}
|
||||
b-button.float-right(v-else variant='success' @click='done') {{edit?$t('Edit'):$t('Send')}}
|
||||
</template>
|
||||
<script>
|
||||
import api from '@/api'
|
||||
import { mapActions, mapState } from 'vuex'
|
||||
import moment from 'moment'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
event: {
|
||||
place: { name: '', address: '' },
|
||||
title: '', description: '', tags: [],
|
||||
multidate: false,
|
||||
},
|
||||
id: null,
|
||||
activeTab: 0,
|
||||
date: null,
|
||||
time: { start: '00:00', end: null },
|
||||
edit: false
|
||||
}
|
||||
},
|
||||
name: 'newEvent',
|
||||
async mounted () {
|
||||
if (this.$route.params.id) {
|
||||
this.id = this.$route.params.id
|
||||
this.edit = true
|
||||
const event = await api.getEvent(this.id)
|
||||
// this.event.place = {name: event.place.name, address: event.place.address }
|
||||
this.event.place.name = event.place.name
|
||||
this.event.place.address = event.place.address || ''
|
||||
this.event.multidate = event.multidate
|
||||
this.date = event.start_datetime
|
||||
this.time.start = moment(event.start_datetime).format('HH:mm')
|
||||
this.time.end = moment(event.end_datetime).format('HH:mm')
|
||||
this.event.title = event.title
|
||||
this.event.description = event.description
|
||||
this.event.id = event.id
|
||||
if (event.tags) {
|
||||
this.event.tags = event.tags.map(t => t.tag)
|
||||
}
|
||||
|
||||
}
|
||||
this.updateMeta()
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
tags: state => state.tags.map(t => t.tag ),
|
||||
places_name: state => state.places.map(p => p.name ),
|
||||
places: state => state.places
|
||||
}),
|
||||
couldProceed () {
|
||||
switch(this.activeTab) {
|
||||
case 0:
|
||||
return this.event.place.name.length>0 &&
|
||||
this.event.place.address.length>0
|
||||
case 1:
|
||||
return true
|
||||
break
|
||||
case 2:
|
||||
return this.event.title.length>0
|
||||
break
|
||||
case 3:
|
||||
return true
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['addEvent', 'updateEvent', 'updateMeta']),
|
||||
next () {
|
||||
this.activeTab++
|
||||
},
|
||||
prev () {
|
||||
this.activeTab--
|
||||
},
|
||||
placeChoosed () {
|
||||
const place = this.places.find( p => p.name === this.event.place.name )
|
||||
if (place && place.address) {
|
||||
this.event.place.address = place.address
|
||||
}
|
||||
this.$refs.address.focus()
|
||||
},
|
||||
async done () {
|
||||
let start_datetime, end_datetime
|
||||
const [ start_hour, start_minute ] = this.time.start.split(':')
|
||||
if (!this.time.end) {
|
||||
this.time.end = this.time.start
|
||||
}
|
||||
const [ end_hour, end_minute ] = this.time.end.split(':')
|
||||
if (this.event.multidate) {
|
||||
start_datetime = moment(this.date.start)
|
||||
.hour(start_hour).minute(start_minute)
|
||||
end_datetime = moment(this.date.end)
|
||||
.hour(end_hour).minute(end_minute)
|
||||
} else {
|
||||
start_datetime = moment(this.date)
|
||||
.hour(start_hour).minute(start_minute)
|
||||
end_datetime = moment(this.date)
|
||||
.hour(end_hour).minute(end_minute)
|
||||
}
|
||||
const formData = new FormData()
|
||||
|
||||
if (this.event.image) {
|
||||
formData.append('image', this.event.image, this.event.image.name)
|
||||
}
|
||||
formData.append('title', this.event.title)
|
||||
formData.append('place_name', this.event.place.name)
|
||||
formData.append('place_address', this.event.place.address)
|
||||
formData.append('description', this.event.description)
|
||||
formData.append('multidate', this.event.multidate)
|
||||
formData.append('start_datetime', start_datetime)
|
||||
formData.append('end_datetime', end_datetime)
|
||||
if (this.edit) {
|
||||
formData.append('id', this.event.id)
|
||||
}
|
||||
if (this.event.tags)
|
||||
this.event.tags.forEach(tag => formData.append('tags[]', tag))
|
||||
|
||||
try {
|
||||
if (this.edit) {
|
||||
await this.updateEvent(formData)
|
||||
} else {
|
||||
await this.addEvent(formData)
|
||||
}
|
||||
this.updateMeta()
|
||||
this.$router.go(-1)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scope>
|
||||
#tabss ul {
|
||||
justify-content: space-evenly;
|
||||
background: linear-gradient( #fff, #FFF 22px, #007bff, #fff 23px, #fff)
|
||||
}
|
||||
|
||||
#tabss ul .nav-link {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#tabss ul .nav-link.active {
|
||||
background-color: #007bff;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user