From f9e0883eaf21dd2b3a1aebcd44944cf3c323ceb3 Mon Sep 17 00:00:00 2001 From: lesion Date: Tue, 23 Jul 2019 01:31:43 +0200 Subject: [PATCH] major on recurrent events --- components/Event.vue | 4 +- components/List.vue | 12 +++++- components/Search.vue | 6 +-- locales/email/it.json | 3 +- locales/it.js | 2 +- nuxt.config.js | 8 +++- package.json | 2 +- pages/add/_edit.vue | 10 +++-- pages/admin.vue | 14 ++++--- pages/event/_id.vue | 50 ++++++++++++++-------- pages/user_confirm/_code.vue | 57 ++++++++++++++++++++++++++ plugins/filters.js | 54 ++++++++++++++++++------ plugins/i18n.js | 4 +- server/api/controller/event.js | 45 +++++++++++++------- server/api/controller/user.js | 4 +- server/api/index.js | 3 +- server/emails/user_confirm/html.pug | 1 + server/emails/user_confirm/subject.pug | 1 + store/index.js | 6 +-- 19 files changed, 210 insertions(+), 76 deletions(-) create mode 100644 pages/user_confirm/_code.vue create mode 100644 server/emails/user_confirm/html.pug create mode 100644 server/emails/user_confirm/subject.pug diff --git a/components/Event.vue b/components/Event.vue index 85a3677c..daa890bb 100644 --- a/components/Event.vue +++ b/components/Event.vue @@ -1,5 +1,5 @@ \ No newline at end of file + diff --git a/pages/admin.vue b/pages/admin.vue index 8ce03d69..055a811e 100644 --- a/pages/admin.vue +++ b/pages/admin.vue @@ -11,18 +11,21 @@ template(slot='label') v-icon(name='users') span.ml-1 {{$t('common.users')}} + + //- ADD NEW USER el-collapse el-collapse-item template(slot='title') - p {{$t('common.new_user')}} + h4 {{$t('common.new_user')}} el-form(inline) el-form-item(:label="$t('common.email')") el-input(v-model='new_user.email') - el-form-item(:label="$t('common.password')") - el-input(v-model='new_user.password' type='password') + //- el-form-item(:label="$t('common.password')") + //- el-input(v-model='new_user.password' type='password') el-form-item(:label="$t('common.admin')") - el-switch(v-model='new_user.admin') + el-switch(v-model='new_user.is_admin') el-button.float-right(@click='create_user' type='success' plain) {{$t('common.send')}} + el-table(:data='paginatedUsers' small) el-table-column(label='Email') template(slot-scope='data') @@ -141,7 +144,6 @@ export default { loading: false, new_user: { email: '', - password: '', admin: false, }, tab: "0", @@ -266,7 +268,7 @@ export default { try { this.loading = true const user = await this.$axios.$post('/user', this.new_user) - this.new_user = { email: '', password: '', is_admin: false } + this.new_user = { email: '', is_admin: false } Message({ showClose: true, type: 'success', diff --git a/pages/event/_id.vue b/pages/event/_id.vue index 7a356e29..06a00193 100644 --- a/pages/event/_id.vue +++ b/pages/event/_id.vue @@ -8,21 +8,21 @@ h5 {{$t('event.not_found')}} div(v-else) - //- title, where, when + //- title h5.text-center {{event.title}} div.nextprev - nuxt-link(v-if='prev' :to='`/event/${prev.id}`') + nuxt-link(v-if='prev' :to='`/event/${prev}`') el-button( type='success' size='mini') v-icon(name='chevron-left') - nuxt-link.float-right(v-if='next' :to='`/event/${next.id}`') + nuxt-link.float-right(v-if='next' :to='`/event/${next}`') el-button(type='success' size='mini') v-icon(name='chevron-right') - + //- image img.main(:src='imgPath' v-if='event.image_path') .info - div {{event|event_when}} + div {{event|when}} div {{event.place.name}} - {{event.place.address}} //- description and tags @@ -61,7 +61,8 @@ import { MessageBox } from 'element-ui' export default { name: 'Event', // transition: null, - // Watch for $route.query.page to call Component methods (asyncData, fetch, validate, layout, etc.) + // Watch for $route.query.page to call + // Component methods (asyncData, fetch, validate, layout, etc.) // watchQuery: ['id'], // Key for (transitions) // key: to => to.fullPath, @@ -77,10 +78,12 @@ export default { title: this.event.title, meta: [ // hid is used as unique identifier. Do not use `vmid` for it as it will not work - { hid: 'description', name: 'description', content: this.event.description.slice(0, 1000) }, - { hid: 'og-description', name: 'og:description', content: this.event.description.slice(0, 100) }, - { hid: 'og-title', property: 'og:title', content: this.event.title }, - { hid: 'og-url', property: 'og:url', content: `event/${this.event.id}` }, + { hid: 'description', name: 'description', + content: this.event.description.slice(0, 1000) }, + { hid: 'og-description', name: 'og:description', + content: this.event.description.slice(0, 100) }, + { hid: 'og-title', property: 'og:title', content: this.event.title }, + { hid: 'og-url', property: 'og:url', content: `event/${this.event.id}` }, { property: 'og:type', content: 'event'}, { property: 'og:image', content: this.imgPath } ] @@ -97,8 +100,10 @@ export default { }, async asyncData ( { $axios, params, error }) { try { - const event = await $axios.$get(`/event/${params.id}`) - return { event, id: params.id } + const [ id, start_datetime ] = params.id.split('_') + const event = await $axios.$get(`/event/${id}`) + event.start_datetime = start_datetime*1000 + return { event, id } } catch(e) { error({ statusCode: 404, message: 'Event not found'}) } @@ -108,18 +113,27 @@ export default { ...mapState(['settings']), next () { let found = false - return this.filteredEvents.find(e => { + const event = this.filteredEvents.find(e => { if (found) return e - if (e.id === this.event.id) found = true + if (e.start_datetime === this.event.start_datetime && e.id === this.event.id) found = true }) + if (!event) return false + if (event.recurrent) { + return `${event.id}_${event.start_datetime/1000}` + } + return event.id }, prev () { - let prev = false + let event = false this.filteredEvents.find(e => { - if (e.id === this.event.id) return true - prev = e + if (e.start_datetime === this.event.start_datetime && e.id === this.event.id) return true + event = e }) - return prev + if (!event) return false + if (event.recurrent) { + return `${event.id}_${event.start_datetime/1000}` + } + return event.id }, imgPath () { return this.event.image_path && '/media/' + this.event.image_path diff --git a/pages/user_confirm/_code.vue b/pages/user_confirm/_code.vue new file mode 100644 index 00000000..c618afdd --- /dev/null +++ b/pages/user_confirm/_code.vue @@ -0,0 +1,57 @@ + + + + diff --git a/plugins/filters.js b/plugins/filters.js index bf607d0b..3440bfbd 100644 --- a/plugins/filters.js +++ b/plugins/filters.js @@ -3,22 +3,52 @@ import moment from 'dayjs' import 'dayjs/locale/it' export default ({ app, store }) => { - moment.locale(store.state.locale) + + // replace links with anchors + // TODO: remove fb tracking id Vue.filter('linkify', value => value.replace(/(https?:\/\/[^\s]+)/g, '$1')) - Vue.filter('datetime', value => moment(value).format('ddd, D MMMM HH:mm')) - Vue.filter('short_datetime', value => moment(value).format('D/MM HH:mm')) - Vue.filter('hour', value => moment(value).format('HH:mm')) - Vue.filter('day', value => moment(value).format('dddd, D MMMM')) - Vue.filter('month', value => moment(value).format('MMM')) - Vue.filter('event_when', event => { - + + // Vue.filter('datetime', value => moment(value).locale(store.state.locale).format('ddd, D MMMM HH:mm')) + // Vue.filter('short_datetime', value => moment(value).locale(store.state.locale).format('D/MM HH:mm')) + // Vue.filter('hour', value => moment(value).locale(store.state.locale).format('HH:mm')) + + // shown in mobile homepage + Vue.filter('day', value => moment(value).locale(store.state.locale).format('dddd, D MMMM')) + // Vue.filter('month', value => moment(value).locale(store.state.locale).format('MMM')) + + // format event start/end datetime based on page + Vue.filter('when', (event, where) => { + moment.locale(store.state.locale) + + //{start,end}_datetime are unix timestamp const start = moment(event.start_datetime) const end = moment(event.end_datetime) + + const normal = `${start.format('dddd, D MMMM (HH:mm-')}${end.format('HH:mm)')}` + + // recurrent event + if (event.recurrent && where !== 'home') { + const { frequency, days, type } = JSON.parse(event.recurrent) + if ( frequency === '1w' || frequency === '2w' ) { + const recurrent = app.i18n.tc(`event.recurrent_${frequency}_days`, days.length, {days: days.map(d => moment().day(d-1).format('dddd'))}) + return `${normal} - ${recurrent}` + } else if (frequency === '1m' || frequency === '2m') { + const d = type === 'ordinal' ? days : days.map(d => moment().day(d-1).format('dddd')) + const recurrent = app.i18n.tc(`event.recurrent_${frequency}_${type}`, days.length, {days: d}) + return `${normal} - ${recurrent}` + } + return 'recurrent ' + } + + // multidate if (event.multidate) { return `${start.format('ddd, D MMMM (HH:mm)')} - ${end.format('ddd, D MMMM')}` - } else if (event.end_datetime && event.end_datetime !== event.start_datetime) - return `${start.format('ddd, D MMMM (HH:mm-')}${end.format('HH:mm)')}` - else - return start.format('dddd, D MMMM (HH:mm)') + } + + // normal event + if (event.end_datetime && event.end_datetime !== event.start_datetime) { + return `${start.format('ddd, D MMMM (HH:mm-')}${end.format('HH:mm)')}` + } + return start.format('dddd, D MMMM (HH:mm)') }) } diff --git a/plugins/i18n.js b/plugins/i18n.js index 9deaa908..00c35d39 100644 --- a/plugins/i18n.js +++ b/plugins/i18n.js @@ -1,7 +1,7 @@ import Vue from 'vue' import VueI18n from 'vue-i18n' -import it from '@/locales/it.js' -import en from '@/locales/en.js' +import it from '../locales/it.js' +import en from '../locales/en.js' Vue.use(VueI18n) diff --git a/server/api/controller/event.js b/server/api/controller/event.js index a4bd63d8..419e7f2f 100644 --- a/server/api/controller/event.js +++ b/server/api/controller/event.js @@ -89,7 +89,13 @@ const eventController = { const id = req.params.event_id let event = await Event.findByPk(id, { plain: true, - attributes: { exclude: ['createdAt', 'updatedAt'] }, + attributes: { + exclude: ['createdAt', 'updatedAt', 'start_datetime', 'end_datetime'], + include: [ + [Sequelize.literal('start_datetime*1000'), 'start_datetime'], + [Sequelize.literal('end_datetime*1000'), 'end_datetime'] + ] + }, include: [ { model: Tag, attributes: ['tag', 'weigth'], through: { attributes: [] } }, { model: Place, attributes: ['name', 'address'] }, @@ -106,7 +112,6 @@ const eventController = { }, async confirm(req, res) { - console.error('confirm event') const id = Number(req.params.event_id) const event = await Event.findByPk(id) if (!event) return res.sendStatus(404) @@ -181,7 +186,8 @@ const eventController = { .year(req.params.year) .month(req.params.month) .startOf('month') - .startOf('isoWeek') + .startOf('week') + console.error('start ', start) let end = moment() .year(req.params.year) @@ -190,7 +196,7 @@ const eventController = { const shownDays = end.diff(start, 'days') if (shownDays <= 35) end = end.add(1, 'week') - end = end.endOf('isoWeek') + end = end.endOf('week') let events = await Event.findAll({ where: { @@ -225,7 +231,7 @@ const eventController = { const recurrent = JSON.parse(e.recurrent) if (!recurrent.frequency) return false - let cursor = moment(start).startOf('isoWeek') + let cursor = moment(start).startOf('week') const start_date = moment(e.start_datetime) const duration = moment(e.end_datetime).diff(start_date, 's') const frequency = recurrent.frequency @@ -234,7 +240,8 @@ const eventController = { // default frequency is '1d' => each day const toAdd = { n: 1, unit: 'day'} - + cursor.set('hour', start_date.hour()).set('minute', start_date.minutes()) + // each week or 2 (search for the first specified day) if (frequency === '1w' || frequency === '2w') { cursor.add(days[0]-1, 'day') @@ -244,23 +251,31 @@ const eventController = { } toAdd.n = Number(frequency[0]) toAdd.unit = 'week'; - cursor.set('hour', start_date.hour()).set('minute', start_date.minutes()) + // cursor.set('hour', start_date.hour()).set('minute', start_date.minutes()) } // each month or 2 - // if (frequency === '1m' || frequency === '2m') { - // // find first match - // if (type) { - - // } - // } + if (frequency === '1m' || frequency === '2m') { + // find first match + toAdd.n = 1 + toAdd.unit = 'month' + if (type === 'weekday') { + + } else if (type === 'ordinal') { + + } + } // add event at specified frequency while (true) { let first_event_of_week = cursor.clone() days.forEach(d => { - cursor.day(d-1) - if (cursor.isAfter(dueTo)) return + if (type === 'ordinal') { + cursor.date(d) + } else { + cursor.day(d-1) + } + if (cursor.isAfter(dueTo) || cursor.isBefore(start)) return e.start_datetime = cursor.unix()*1000 e.end_datetime = e.start_datetime+(duration*1000)// cursor.clone().hour(end_datetime.hour()).minute(end_datetime.minute()).unix()*1000 events.push( Object.assign({}, e) ) diff --git a/server/api/controller/user.js b/server/api/controller/user.js index 15f1445d..b7d3da21 100644 --- a/server/api/controller/user.js +++ b/server/api/controller/user.js @@ -33,7 +33,7 @@ const userController = { }, config.secret ) - + res.cookie('auth._token.local', 'Bearer ' + accessToken) res.json({ token: accessToken }) } } @@ -259,7 +259,9 @@ const userController = { async create(req, res) { try { req.body.is_active = true + req.body.recover_code = crypto.randomBytes(16).toString('hex') const user = await User.create(req.body) + mail.send(user.email, 'user_confirm', { user, config }) res.json(user) } catch (e) { res.status(404).json(e) diff --git a/server/api/index.js b/server/api/index.js index 49171d89..c32701f8 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -12,13 +12,12 @@ const userController = require('./controller/user') const settingsController = require('./controller/settings') const storage = require('./storage') - const upload = multer({ storage }) + const api = express.Router() api.use(cookieParser()) api.use(bodyParser.urlencoded({ extended: false })) api.use(bodyParser.json()) -// api.use(settingsController.init) const jwt = expressJwt({ secret: config.secret, diff --git a/server/emails/user_confirm/html.pug b/server/emails/user_confirm/html.pug new file mode 100644 index 00000000..4228d6d8 --- /dev/null +++ b/server/emails/user_confirm/html.pug @@ -0,0 +1 @@ +p !{t('email.user_confirm', { config, user })} diff --git a/server/emails/user_confirm/subject.pug b/server/emails/user_confirm/subject.pug new file mode 100644 index 00000000..6d0067a6 --- /dev/null +++ b/server/emails/user_confirm/subject.pug @@ -0,0 +1 @@ += `[Gancio] Richiesta password recovery` diff --git a/store/index.js b/store/index.js index 1394569c..7700851b 100644 --- a/store/index.js +++ b/store/index.js @@ -1,7 +1,5 @@ import moment from 'dayjs' import intersection from 'lodash/intersection' -import map from 'lodash/map' -import filter from 'lodash/filter' import find from 'lodash/find' export const state = () => ({ @@ -147,10 +145,8 @@ export const actions = { commit('setSettings', settings) // apply settings - commit('showRecurrentEvents', settings.recurrent_event_visible) + commit('showRecurrentEvents', settings.allow_recurrent_event && settings.recurrent_event_visible) - const lang = req.acceptsLanguages('en', 'it') - commit('setLocale', lang || 'it') }, async updateEvents({ commit }, page) { const events = await this.$axios.$get(`/event/${page.month - 1}/${page.year}`)