diff options
author | dartcafe <github@dartcafe.de> | 2019-12-10 09:32:58 +0300 |
---|---|---|
committer | dartcafe <github@dartcafe.de> | 2019-12-10 09:32:58 +0300 |
commit | 41930996829421bbf5e26aeb25c93d9214d35e9c (patch) | |
tree | 217e7fe3ed394256a324ed7bcbf81c89f0a0c19e | |
parent | 7363c382904c6ea909010a4939e4ecd40dfdb5eb (diff) |
- moved src back in right place
45 files changed, 5527 insertions, 0 deletions
diff --git a/src/js/App.vue b/src/js/App.vue new file mode 100644 index 00000000..59c8611a --- /dev/null +++ b/src/js/App.vue @@ -0,0 +1,137 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div id="app-polls"> + <Navigation v-if="loadNavigation" /> + <router-view /> + </div> +</template> + +<script> +import Navigation from './components/Navigation/Navigation' + +export default { + name: 'App', + components: { + Navigation + }, + + data() { + return { + loadNavigation: false + } + }, + + computed: { + isPublic() { + return (this.$route.name === 'publicVote') + } + }, + + watch: { + '$route'(to, from) { + this.loadNavigation = (this.$route.name !== 'publicVote') + } + } +} + +</script> + +<style lang="scss"> + +.list-enter-active, +.list-leave-active { + transition: all 0.5s ease; +} + +.list-enter, +.list-leave-to { + opacity: 0; +} + +.list-move { + transition: transform 0.5s; +} + +.fade-leave-active { + transition: opacity 2.5s; +} +.fade-enter, .fade-leave-to { + opacity: 0; +} + +#app-polls { + width: 100%; + // display: flex; +} + +#app-content { + display: flex; + width: auto; + + input { + &.hasTimepicker { + width: 75px; + } + &.error { + border-color: var(--color-error); + background-color: #f9c5c5; + background-image: var(--icon-error-e9322d); + background-repeat: no-repeat; + background-position: right; + } + &.success, &.icon-confirn.success { + border-color: var(--color-success); + background-color: #d6fdda !important; + &.icon-confirm { + border-color: var(--color-success) !important; + border-left-color: transparent !important; + } + } + + &.icon { + flex: 0; + padding: 0 17px; + } + } + + .label { + border: solid 1px; + border-radius: var(--border-radius); + padding: 1px 4px; + margin: 0 4px; + font-size: 60%; + text-align: center; + &.error { + border-color: var(--color-error); + background-color: var(--color-error); + color: var(--color-primary-text); + } + &.success { + border-color: var(--color-success); + background-color: var(--color-success); + color: var(--color-primary-text); + } + } +} +</style> diff --git a/src/js/assets/app.png b/src/js/assets/app.png Binary files differnew file mode 100644 index 00000000..84a879bb --- /dev/null +++ b/src/js/assets/app.png diff --git a/src/js/assets/app.svg b/src/js/assets/app.svg new file mode 100644 index 00000000..cbf61396 --- /dev/null +++ b/src/js/assets/app.svg @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + id="svg8" + viewBox="0 0 32 32" + x="0px" + y="0px" + enable-background="new 0 0 595.275 311.111" + width="32" + height="32" + xml:space="preserve" + version="1.1"><metadata + id="metadata14"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs + id="defs12" /><rect + id="rect2" + y="2" + x="3" + height="26" + width="7" + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.93541431" /><rect + id="rect4" + y="12" + x="12" + height="16" + width="7" + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.93541431" /><rect + id="rect6" + y="8" + x="21" + height="20" + width="7" + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.8918826" /></svg>
\ No newline at end of file diff --git a/src/js/components/Base/LoadingOverlay.vue b/src/js/components/Base/LoadingOverlay.vue new file mode 100644 index 00000000..c8148854 --- /dev/null +++ b/src/js/components/Base/LoadingOverlay.vue @@ -0,0 +1,59 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div class="loading-overlay"> + <span class="icon-loading" /> + </div> +</template> + +<script> +export default { + name: 'LoadingOverlay' +} +</script> + +<style lang="scss"> +.loading-overlay { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: #fff; + opacity: 0.9; + z-index: 1001; + .icon-loading { + position: fixed; + left: 50%; + top: 50%; + margin-left: -35px; + margin-top: -10px; + &::after { + border: 10px solid var(--color-loading-light); + border-top-color: var(--color-primary-element); + height: 70px; + width: 70px; + } + } +} +</style> diff --git a/src/js/components/Base/UserDiv.vue b/src/js/components/Base/UserDiv.vue new file mode 100644 index 00000000..231d9f5c --- /dev/null +++ b/src/js/components/Base/UserDiv.vue @@ -0,0 +1,148 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +/* global Vue, oc_userconfig */ +<template> + <div class="user-row" :class="type"> + <div v-if="description" class="description"> + {{ description }} + </div> + <Avatar :disable-menu="true" :user="userId" :display-name="computedDisplayName" + :is-no-user="isNoUser" /> + <div class="avatar" :class="iconClass" /> + + <div v-if="!hideNames" class="user-name"> + {{ computedDisplayName }} + </div> + </div> +</template> + +<script> +import { Avatar } from '@nextcloud/vue' + +export default { + name: 'UserDiv', + + components: { + Avatar + }, + + props: { + hideNames: { + type: Boolean, + default: false + }, + userId: { + type: String, + default: undefined + }, + displayName: { + type: String, + default: '' + }, + type: { + type: String, + default: 'user' + }, + description: { + type: String, + default: '' + }, + icon: { + type: Boolean, + default: false + } + + }, + + data() { + return { + nothidden: false + } + }, + + computed: { + isNoUser() { + return this.type !== 'user' + }, + + isValidUser() { + return (this.userId !== '' && this.userId !== null) + }, + + iconClass() { + if (this.icon) { + return 'icon-' + this.type + } else { + return '' + } + }, + + computedDisplayName() { + let value = this.displayName + + if (this.userId === OC.getCurrentUser().uid) { + value = OC.getCurrentUser().displayName + } else { + if (!this.displayName) { + value = this.userId + } + } + if (this.type === 'group') { + value = value + ' (' + t('polls', 'Group') + ')' + } + return value + } + + } +} +</script> + +<style lang="scss"> +.user-row { + display: flex; + flex: 1; + align-items: center; + margin-left: 0; + margin-top: 0; + + > div { + margin: 2px 4px; + } + + .description { + opacity: 0.7; + flex: 0; + } + + .avatar { + height: 32px; + width: 32px; + flex: 0; + } + + .user-name { + opacity: 0.5; + flex: 1; + } +} +</style> diff --git a/src/js/components/Create/CreateDateItem.vue b/src/js/components/Create/CreateDateItem.vue new file mode 100644 index 00000000..eaebe234 --- /dev/null +++ b/src/js/components/Create/CreateDateItem.vue @@ -0,0 +1,71 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <li> + <div>{{ option.timestamp | localFullDate }}</div> + <div> + <a class="icon-delete" @click="$emit('remove')" /> + </div> + </li> +</template> + +<script> +import moment from 'moment' + +export default { + name: 'DatePollItem', + + filters: { + localFullDate(timestamp) { + if (!timestamp) return '' + if (timestamp < 999999999999) timestamp = timestamp * 1000 + if (!moment(timestamp).isValid()) return 'Invalid Date' + return moment(timestamp).format('llll') + } + }, + + props: { + option: { + type: Object, + default: undefined + } + } +} +</script> + +<style lang="scss" scoped> + li > div { + display: flex; + flex-grow: 1; + font-size: 1.2em; + opacity: 0.7; + white-space: normal; + padding-right: 4px; + } + + li > div:nth-last-child(1) { + justify-content: center; + flex-grow: 0; + flex-shrink: 0; + } +</style> diff --git a/src/js/components/Create/CreateDlg.vue b/src/js/components/Create/CreateDlg.vue new file mode 100644 index 00000000..4330abb9 --- /dev/null +++ b/src/js/components/Create/CreateDlg.vue @@ -0,0 +1,125 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template lang="html"> + <div class="create-dialog"> + <h2>{{ t('polls', 'Create new poll') }}</h2> + <input id="pollTitle" v-model="title" type="text" + :placeholder="t('polls', 'Enter Title')"> + + <div class="configBox"> + <label class="title icon-checkmark"> + {{ t('polls', 'Poll type') }} + </label> + <input id="datePoll" v-model="type" value="datePoll" + :disabled="protect" type="radio" class="radio"> + <label for="datePoll"> + {{ t('polls', 'Event schedule') }} + </label> + <input id="textPoll" v-model="type" value="textPoll" + :disabled="protect" type="radio" class="radio"> + <label for="textPoll"> + {{ t('polls', 'Text based') }} + </label> + </div> + + <div class="create-buttons"> + <button class="button" @click="cancel"> + {{ t('polls', 'Cancel') }} + </button> + <button :disabled="titleEmpty" class="button primary" @click="confirm"> + {{ t('polls', 'Publish') }} + </button> + </div> + </div> +</template> + +<script> +import { mapState, mapMutations } from 'vuex' +export default { + name: 'CreateDlg', + + data() { + return { + id: 0, + type: 'datePoll', + title: '' + } + }, + + computed: { + ...mapState({ + event: state => state.event + }), + + titleEmpty() { + return this.title === '' + } + }, + + methods: { + ...mapMutations([ 'setEventProperty', 'resetEvent', 'reset' ]), + + cancel() { + this.title = '' + this.type = 'datePoll' + this.$emit('closeCreate') + }, + + confirm() { + this.resetEvent() + this.reset() + this.setEventProperty({ 'id': 0 }) + this.setEventProperty({ 'title': this.title }) + this.setEventProperty({ 'type': this.type }) + this.$store.dispatch('writeEventPromise') + .then((response) => { + this.cancel() + OC.Notification.showTemporary(t('polls', 'Poll "%n" added', 1, this.event.title), { type: 'success' }) + this.$router.push({ name: 'vote', params: { id: this.event.id } }) + }) + .catch(() => { + OC.Notification.showTemporary(t('polls', 'Error while creating Poll "%n"', 1, this.event.title), { type: 'error' }) + }) + } + } + +} +</script> + +<style lang="css" scoped> +.create-dialog { + display: flex; + flex-direction: column; + background-color: var(--color-main-background); + padding: 20px; +} + +#pollTitle { + width: 100%; +} + +.create-buttons { + display: flex; + justify-content: space-between; +} +</style> diff --git a/src/js/components/Create/TextPollItem.vue b/src/js/components/Create/TextPollItem.vue new file mode 100644 index 00000000..aefd8d09 --- /dev/null +++ b/src/js/components/Create/TextPollItem.vue @@ -0,0 +1,44 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <li> + <div>{{ option.pollOptionText }}</div> + <div> + <a class="icon icon-delete svg delete-poll" @click="$emit('remove')" /> + </div> + </li> +</template> + +<script> +export default { + name: 'TextPollItem', + + props: { + option: { + type: Object, + default: undefined + } + } +} + +</script> diff --git a/src/js/components/Navigation/Navigation.vue b/src/js/components/Navigation/Navigation.vue new file mode 100644 index 00000000..cafa37d5 --- /dev/null +++ b/src/js/components/Navigation/Navigation.vue @@ -0,0 +1,191 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template lang="html"> + <AppNavigation> + <AppNavigationNew :text="t('polls', 'Add new Poll')" @click="toggleCreateDlg" /> + <CreateDlg v-show="createDlg" @closeCreate="closeCreate()" /> + <ul> + <AppNavigationItem + :title="t('polls', 'All polls')" + :allow-collapse="true" + icon="icon-folder" + :to="{ name: 'list', params: {type: 'all'}}" + :open="true"> + <ul> + <AppNavigationItem + v-for="(poll) in allPolls" + :key="poll.id" + :title="poll.title" + :icon="eventIcon(poll.type)" + :to="{name: 'vote', params: {id: poll.id}}" /> + </ul> + </AppNavigationItem> + <AppNavigationItem + :title="t('polls', 'My polls')" + :allow-collapse="true" + icon="icon-user" + :to="{ name: 'list', params: {type: 'my'}}" + :open="false"> + <ul> + <AppNavigationItem + v-for="(poll) in myPolls" + :key="poll.id" + :title="poll.title" + :icon="eventIcon(poll.type)" + :to="{name: 'vote', params: {id: poll.id}}" /> + </ul> + </AppNavigationItem> + <AppNavigationItem + :title="t('polls', 'Public polls')" + :allow-collapse="true" + icon="icon-link" + :to="{ name: 'list', params: {type: 'public'}}" + :open="false"> + <ul> + <AppNavigationItem + v-for="(poll) in publicPolls" + :key="poll.id" + :title="poll.title" + :icon="eventIcon(poll.type)" + :to="{name: 'vote', params: {id: poll.id}}" /> + </ul> + </AppNavigationItem> + <AppNavigationItem + :title="t('polls', 'Hidden polls')" + :allow-collapse="true" + icon="icon-password" + :to="{ name: 'list', params: {type: 'hidden'}}" + :open="false"> + <ul> + <AppNavigationItem + v-for="(poll) in hiddenPolls" + :key="poll.id" + :title="poll.title" + :icon="eventIcon(poll.type)" + :to="{name: 'vote', params: {id: poll.id}}" /> + </ul> + </AppNavigationItem> + <AppNavigationItem + :title="t('polls', 'Deleted polls')" + :allow-collapse="true" + icon="icon-delete" + :to="{ name: 'list', params: {type: 'deleted'}}" + :open="false"> + <ul> + <AppNavigationItem + v-for="(poll) in deletedPolls" + :key="poll.id" + :title="poll.title" + :icon="eventIcon(poll.type)" + :to="{name: 'vote', params: {id: poll.id}}" /> + </ul> + </AppNavigationItem> + </ul> + + <AppNavigationSettings> + <router-link :to="{ name: 'list'}"> + List + </router-link> + </AppNavigationSettings> + </AppNavigation> +</template> + +<script> + +import { AppNavigation, AppNavigationNew, AppNavigationItem, AppNavigationSettings } from '@nextcloud/vue' +import { mapGetters } from 'vuex' +import CreateDlg from '../Create/CreateDlg' +import state from './store/polls.js' + +export default { + name: 'Navigation', + components: { + AppNavigation, + AppNavigationNew, + AppNavigationItem, + AppNavigationSettings, + CreateDlg + }, + + data() { + return { + createDlg: false + } + }, + + computed: { + + ...mapGetters([ + 'allPolls', + 'myPolls', + 'publicPolls', + 'hiddenPolls', + 'deletedPolls' + ]), + + pollList() { + return this.$store.state.polls.list + } + }, + + created() { + this.$store.registerModule('polls', state) + this.refreshPolls() + }, + + methods: { + closeCreate() { + this.createDlg = false + }, + + toggleCreateDlg() { + this.createDlg = !this.createDlg + }, + + eventIcon(type) { + if (type === '0') { + return 'icon-calendar' + } else { + return 'icon-toggle-filelist' + } + }, + + refreshPolls() { + if (this.$route.name !== 'publicVote') { + + this.loading = true + this.$store + .dispatch('loadPolls') + .then(response => { + this.loading = false + }) + .catch(error => { + this.loading = false + console.error('refresh poll: ', error.response) + OC.Notification.showTemporary(t('polls', 'Error loading polls'), { type: 'error' }) + }) + } + } + } +} +</script> diff --git a/src/js/components/Navigation/store/polls.js b/src/js/components/Navigation/store/polls.js new file mode 100644 index 00000000..0f528579 --- /dev/null +++ b/src/js/components/Navigation/store/polls.js @@ -0,0 +1,75 @@ +/* + * @copyright Copyright (c) 2019 Rene Gieling <github@dartcafe.de> + * + * @author Rene Gieling <github@dartcafe.de> + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import axios from '@nextcloud/axios' + +const state = { + list: [] +} + +const mutations = { + setPolls(state, { list }) { + state.list = list + } +} + +const getters = { + countPolls: (state) => { + return state.list.length + }, + allPolls: (state) => { + return state.list.filter(poll => (!poll.deleted)) + }, + myPolls: (state) => { + return state.list.filter(poll => (poll.owner === OC.getCurrentUser().uid)) + }, + publicPolls: (state) => { + return state.list.filter(poll => (poll.access === 'public')) + }, + hiddenPolls: (state) => { + return state.list.filter(poll => (poll.access === 'hidden')) + }, + deletedPolls: (state) => { + return state.list.filter(poll => (poll.deleted)) + } +} + +const actions = { + loadPolls({ commit }) { + return axios.get(OC.generateUrl('apps/polls/get/events')) + .then((response) => { + commit('setPolls', { list: response.data }) + }, (error) => { + console.error(error.response) + }) + }, + + deletePollPromise(context, payload) { + return axios.post( + OC.generateUrl('apps/polls/remove/poll'), + payload.event + ) + } +} + +export default { state, mutations, getters, actions } diff --git a/src/js/components/Notification/Notification.vue b/src/js/components/Notification/Notification.vue new file mode 100644 index 00000000..930721c6 --- /dev/null +++ b/src/js/components/Notification/Notification.vue @@ -0,0 +1,64 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template lang="html"> + <div class="notification"> + <input id="subscribe" v-model="subscribe" type="checkbox" + class="checkbox"> + <label for="subscribe">{{ t('polls', 'Receive notification email on activity') }}</label> + </div> +</template> + +<script> +import { mapState } from 'vuex' +export default { + name: 'Notification', + + computed: { + ...mapState({ + notification: state => state.notification, + event: state => state.event + }), + + subscribe: { + get() { + return this.notification.subscribed + }, + set(value) { + this.$store.commit('setNotification', value) + this.$store.dispatch('writeSubscriptionPromise', { pollId: this.event.id }) + } + } + }, + + mounted() { + this.$store.dispatch('getSubscription', { pollId: this.$route.params.id }) + } + +} +</script> + +<style lang="css" scoped> + .notification { + padding: 24px; + } +</style> diff --git a/src/js/components/PollList/PollListItem.vue b/src/js/components/PollList/PollListItem.vue new file mode 100644 index 00000000..95c0468a --- /dev/null +++ b/src/js/components/PollList/PollListItem.vue @@ -0,0 +1,490 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div v-if="header" class="pollListItem header"> + <div class="title"> + {{ t('polls', 'Title') }} + </div> + + <div class="access"> + {{ t('polls', 'Access') }} + </div> + + <div class="owner"> + {{ t('polls', 'Owner') }} + </div> + + <div class="dates"> + <div class="created"> + {{ t('polls', 'Created') }} + </div> + <div class="expiry"> + {{ t('polls', 'Expires') }} + </div> + </div> + </div> + + <div v-else class="pollListItem poll"> + <div v-tooltip.auto="pollType" class="thumbnail" :class="[pType, {expired : poll.expired}]"> + {{ pollType }} + </div> + + <!-- <div v-if="votedBycurrentUser" class="symbol icon-voted" /> --> + + <router-link :to="{name: 'vote', params: {id: poll.id}}" class="title"> + <div class="name"> + {{ poll.title }} + </div> + <div class="description"> + {{ poll.description }} + </div> + </router-link> + + <!-- <div v-if="countComments" v-tooltip.auto="countCommentsHint" class="app-navigation-entry-utils-counter highlighted"> + <span>{{ countComments }}</span> + </div> --> + + <div class="actions"> + <div class="toggleUserActions"> + <div v-click-outside="hideMenu" class="icon-more" @click="toggleMenu" /> + <div class="popovermenu" :class="{ 'open': openedMenu }"> + <popover-menu :menu="menuItems" /> + </div> + </div> + </div> + + <div v-tooltip.auto="accessType" class="thumbnail access" :class="aType"> + {{ accessType }} + </div> + + <div class="owner"> + <user-div :user-id="poll.owner" :display-name="poll.ownerDisplayName" /> + </div> + + <div class="dates"> + <div class="created "> + {{ timeSpanCreated }} + </div> + <div class="expiry" :class="{ expired : poll.expired }"> + {{ timeSpanExpiration }} + </div> + </div> + </div> +</template> + +<script> +import moment from 'moment' + +export default { + name: 'PollListItem', + + props: { + header: { + type: Boolean, + default: false + }, + poll: { + type: Object, + default: undefined + } + }, + + data() { + return { + openedMenu: false, + hostName: this.$route.query.page + } + }, + + computed: { + + // TODO: dity hack + aType() { + if (this.poll.access === 'public') { + return this.poll.access + } else if (this.poll.access === 'registered') { + return this.poll.access + } else if (this.poll.access === 'hidden') { + return this.poll.access + } else { + return 'select' + } + }, + + expired() { + if (this.poll.expire === null) { + return false + } else if (Date.parse(this.poll.expire) < Date.now()) { + return false + } else { + return true + } + }, + + accessType() { + if (this.aType === 'public') { + return t('polls', 'Public access') + } else if (this.aType === 'registered') { + return t('polls', 'Registered users only') + } else if (this.aType === 'hidden') { + return t('polls', 'Hidden poll') + } else if (this.aType === 'select') { + return t('polls', 'Only shared') + } else { + return '' + } + }, + + // TODO: dity hack + pType() { + if (this.poll.type === '1') { + // TRANSLATORS This means that this is the type of the poll. Another type is a 'date poll'. + return t('polls', 'textPoll') + } else { + // TRANSLATORS This means that this is the type of the poll. Another type is a 'text poll'. + return t('polls', 'datePoll') + } + + }, + + pollType() { + if (this.pType === 'textPoll') { + // TRANSLATORS This means that this is the type of the poll. Another type is a 'date poll'. + return t('polls', 'Text poll') + } else { + // TRANSLATORS This means that this is the type of the poll. Another type is a 'text poll'. + return t('polls', 'Date poll') + } + }, + + timeSpanCreated() { + return moment(this.poll.created).fromNow() + }, + + timeSpanExpiration() { + if (this.poll.expire) { + return moment(this.poll.expire).fromNow() + } else { + return t('polls', 'never') + } + }, + + voteUrl() { + return OC.generateUrl('apps/polls/poll/') + this.poll.id + }, + + menuItems() { + let items = [ + { + key: 'copyLink', + icon: 'icon-clippy', + text: t('polls', 'Copy Link'), + action: this.copyLink + }, + { + key: 'clonePoll', + icon: 'icon-confirm', + text: t('polls', 'Clone poll'), + action: this.clonePoll + } + ] + + if (this.poll.owner === OC.getCurrentUser().uid) { + // items.push({ + // key: 'editPoll', + // icon: 'icon-rename', + // text: t('polls', 'Edit poll'), + // action: this.editPoll + // }) + items.push({ + key: 'deletePoll', + icon: 'icon-delete', + text: t('polls', 'Delete poll'), + action: this.deletePoll + }) + } else if (OC.isUserAdmin()) { + // items.push({ + // key: 'editPoll', + // icon: 'icon-rename', + // text: t('polls', 'Edit poll as admin'), + // action: this.editPoll + // }) + items.push({ + key: 'deletePoll', + icon: 'icon-delete', + text: t('polls', 'Delete poll as admin'), + action: this.deletePoll + }) + } + + return items + } + }, + + methods: { + toggleMenu() { + this.openedMenu = !this.openedMenu + }, + + hideMenu() { + this.openedMenu = false + }, + + copyLink() { + // this.$emit('copyLink') + this.$copyText(window.location.origin + this.voteUrl).then( + function(e) { + OC.Notification.showTemporary(t('polls', 'Link copied to clipboard'), { type: 'success' }) + }, + function(e) { + OC.Notification.showTemporary(t('polls', 'Error, while copying link to clipboard'), { type: 'error' }) + } + ) + this.hideMenu() + }, + + deletePoll() { + this.$emit('deletePoll') + this.hideMenu() + }, + + votePoll() { + this.$emit('votePoll') + this.hideMenu() + }, + + editPoll() { + this.$emit('editPoll') + this.hideMenu() + }, + + clonePoll() { + this.$emit('clonePoll') + this.hideMenu() + } + } +} +</script> + +<style lang="scss" scoped> + +.pollListItem { + display: flex; + flex: 1; + padding-left: 8px; + &.header { + opacity: 0.5; + flex: auto; + height: 4em; + align-items: center; + margin-left: 44px; + } + &> div { + padding-right: 8px; + } +} + +.thumbnail { + flex: 0 0 auto; +} + +.icon-more { + right: 14px; + opacity: 0.3; + cursor: pointer; + height: 44px; + width: 44px; +} + +.title { + display: flex; + flex-direction: column; + align-items: stretch; + width: 210px; + flex: 1 1 auto; + .name, + .description { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .description { + opacity: 0.5; + } +} + +.thumbnail.access, .owner { + flex: 0 0 auto; +} + +.thumbnail.access { + width: 75px; +} + +.owner { + width: 130px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.actions { + width: 44px; + align-items: center; + position: relative; +} + +.dates { + display: flex; + flex-wrap: wrap; + align-items: center; + + .created, .expiry { + width: 100px; + flex: 1 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } +} + + .thumbnail { + width: 44px; + height: 44px; + padding-right: 4px; + font-size: 0; + background-color: var(--color-text-light); + &.datePoll { + mask-image: var(--icon-calendar-000) no-repeat 50% 50%; + -webkit-mask: var(--icon-calendar-000) no-repeat 50% 50%; + mask-size: 16px; + } + &.textPoll { + mask-image: var(--icon-organization-000) no-repeat 50% 50%; + -webkit-mask: var(--icon-organization-000) no-repeat 50% 50%; + mask-size: 16px; + } + &.expired { + background-color: var(--color-background-darker); + } + &.access { + display: inherit; + &.hidden { + mask-image: var(--icon-password-000) no-repeat 50% 50%; + -webkit-mask: var(--icon-password-000) no-repeat 50% 50%; + mask-size: 16px; + } + &.public { + mask-image: var(--icon-link-000) no-repeat 50% 50%; + -webkit-mask: var(--icon-link-000) no-repeat 50% 50%; + mask-size: 16px; + } + &.select { + mask-image: var(--icon-share-000) no-repeat 50% 50%; + -webkit-mask: var(--icon-share-000) no-repeat 50% 50%; + mask-size: 16px; + } + &.registered { + mask-image: var(--icon-group-000) no-repeat 50% 50%; + -webkit-mask: var(--icon-group-000) no-repeat 50% 50%; + mask-size: 16px; + } + } + } + + .icon-voted { + background-image: var(--icon-checkmark-fff); + } + + .comment-badge { + position: absolute; + top: 0; + width: 26px; + line-height: 26px; + text-align: center; + font-size: 0.7rem; + color: white; + background-image: var(--icon-comment-49bc49); + background-repeat: no-repeat; + background-size: 26px; + z-index: 1; + } + + .app-navigation-entry-utils-counter { + padding-right: 0 !important; + overflow: hidden; + text-align: right; + font-size: 9pt; + line-height: 44px; + padding: 0 12px; + // min-width: 25px; + &.highlighted { + padding: 0; + text-align: center; + span { + padding: 2px 5px; + border-radius: 10px; + background-color: var(--color-primary); + color: var(--color-primary-text); + } + } + } + + .symbol.icon-voted { + position: absolute; + left: 11px; + top: 16px; + background-size: 0; + min-width: 8px; + min-height: 8px; + background-color: var(--color-success); + border-radius: 50%; + } + + @media all and (max-width: (740px)) { + .dates { + flex-direction: column; + } + } + + @media all and (max-width: (620px)) { + .owner { + display: none; + } + } + + @media all and (max-width: (490px)) { + .dates { + display: none; + } + } + + @media all and (max-width: (380px)) { + .thumbnail.access, .access { + width: 140px; + display: none; + } + } + +</style> diff --git a/src/js/components/Share/ShareDiv.vue b/src/js/components/Share/ShareDiv.vue new file mode 100644 index 00000000..f9fa2e1b --- /dev/null +++ b/src/js/components/Share/ShareDiv.vue @@ -0,0 +1,172 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div> + <h2> {{ t('polls', 'Share with') }}</h2> + + <Multiselect id="ajax" + v-model="shares" + :options="users" + :multiple="true" + :user-select="true" + :tag-width="80" + :clear-on-select="false" + :preserve-search="true" + :options-limit="20" + :loading="isLoading" + :internal-search="false" + :searchable="true" + :preselect-first="true" + :placeholder="placeholder" + label="displayName" + track-by="user" + @search-change="loadUsersAsync" + @close="updateShares"> + <template slot="selection" slot-scope="{ values, search, isOpen }"> + <span v-if="values.length && !isOpen" class="multiselect__single"> + {{ values.length }} users selected + </span> + </template> + </Multiselect> + + <TransitionGroup :css="false" tag="ul" class="shared-list"> + <li v-for="(item, index) in sortedShares" :key="item.displayName" :data-index="index"> + <UserDiv :user-id="item.user" :display-name="item.displayName" :type="item.type" + :hide-names="hideNames" /> + <div class="options"> + <a class="icon icon-delete svg delete-poll" @click="removeShare(index, item)" /> + </div> + </li> + </TransitionGroup> + </div> +</template> + +<script> +import { Multiselect } from '@nextcloud/vue' + +export default { + name: 'ShareDiv', + + components: { + Multiselect + }, + + props: { + placeholder: { + type: String, + default: '' + }, + + activeShares: { + type: Array, + default: function() { + return [] + } + }, + + hideNames: { + type: Boolean, + default: false + } + }, + + data() { + return { + shares: [], + users: [], + isLoading: false, + siteUsersListOptions: { + getUsers: true, + getGroups: true, + query: '' + } + } + }, + + computed: { + sortedShares() { + return this.shares.slice(0).sort(this.sortByDisplayname) + } + }, + + watch: { + activeShares(value) { + this.shares = value.slice(0) + } + }, + + methods: { + removeShare(index, item) { + this.$emit('remove-share', item) + }, + + updateShares() { + this.$emit('update-shares', this.shares) + }, + + loadUsersAsync(query) { + this.isLoading = false + this.siteUsersListOptions.query = query + this.$http.post(OC.generateUrl('apps/polls/get/siteusers'), this.siteUsersListOptions) + .then((response) => { + this.users = response.data.siteusers + this.isLoading = false + }, (error) => { + console.error(error.response) + }) + }, + + sortByDisplayname(a, b) { + if (a.displayName.toLowerCase() < b.displayName.toLowerCase()) return -1 + if (a.displayName.toLowerCase() > b.displayName.toLowerCase()) return 1 + return 0 + } + + } +} +</script> + +<style lang="scss"> + .shared-list { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + padding-top: 8px; + + > li { + display: flex; + } + } + + .options { + display: flex; + position: relative; + top: -12px; + left: -13px; + } + + .multiselect { + width: 100% !important; + max-width: 100% !important; + } +</style> diff --git a/src/js/components/SideBar/CommentAdd.vue b/src/js/components/SideBar/CommentAdd.vue new file mode 100644 index 00000000..2d7f054a --- /dev/null +++ b/src/js/components/SideBar/CommentAdd.vue @@ -0,0 +1,99 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template lang="html"> + <div class="newCommentRow comment new-comment"> + <user-div :user-id="currentUser" /> + + <form class="commentAdd" name="send_comment"> + <input v-model="comment" class="message" data-placeholder="New Comment ..."> + <input v-show="!loading" class="submitComment icon-confirm" @click="writeComment"> + <span v-show="loading" class="icon-loading-small" style="float:right;" /> + </form> + </div> +</template> + +<script> +export default { + name: 'AddComment', + data() { + return { + comment: '' + } + }, + + computed: { + currentUser() { + return OC.getCurrentUser().uid + } + }, + + methods: { + writeComment() { + this.$store.dispatch('writeCommentPromise', this.comment) + .then(response => { + OC.Notification.showTemporary(t('polls', 'Your comment was added'), { type: 'success' }) + }) + .catch(error => { + this.writingVote = false + console.error('Error while saving comment - Error: ', error.response) + OC.Notification.showTemporary(t('polls', 'Error while saving comment'), { type: 'error' }) + }) + + } + } +} +</script> + +<style lang="scss" scoped> + .comment { + margin-bottom: 30px; + } + + .commentAdd { + display: flex; + } + + .message { + margin-left: 40px; + flex: 1; + &:empty:before { + content: attr(data-placeholder); + color: grey; + } + } + .submitComment { + align-self: last baseline; + width: 30px; + margin: 0; + padding: 7px 9px; + background-color: transparent; + border: none; + opacity: 0.3; + cursor: pointer; + } + + .icon-loading-small { + float: left; + margin-top: 10px; + } +</style> diff --git a/src/js/components/SideBar/SideBar.vue b/src/js/components/SideBar/SideBar.vue new file mode 100644 index 00000000..30931f01 --- /dev/null +++ b/src/js/components/SideBar/SideBar.vue @@ -0,0 +1,109 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <AppSidebar :active="initialTab" :title="t('polls', 'Details')" @close="$emit('closeSideBar')"> + <UserDiv slot="primary-actions" :user-id="event.owner" :description="t('polls', 'Owner')" /> + + <AppSidebarTab :name="t('polls', 'Comments')" icon="icon-comment"> + <SideBarTabComments /> + </AppSidebarTab> + + <AppSidebarTab v-if="acl.allowEdit && event.type === 'datePoll'" :name="t('polls', 'Date options')" icon="icon-calendar"> + <SideBarTabDateOptions /> + </AppSidebarTab> + + <AppSidebarTab v-if="acl.allowEdit && event.type === 'textPoll'" :name="t('polls', 'Text options')" icon="icon-toggle-filelist"> + <SideBarTabTextOptions /> + </AppSidebarTab> + + <AppSidebarTab v-if="acl.allowEdit" :name="t('polls', 'Configuration')" icon="icon-settings"> + <SideBarTabConfiguration @deletePoll="$emit('deletePoll')" /> + </AppSidebarTab> + + <AppSidebarTab v-if="acl.allowEdit" :name="t('polls', 'Shares')" icon="icon-share"> + <SideBarTabShare /> + </AppSidebarTab> + </AppSidebar> +</template> + +<script> +import { AppSidebar, AppSidebarTab } from '@nextcloud/vue' + +import SideBarTabConfiguration from './SideBarTabConfiguration' +import SideBarTabDateOptions from './SideBarTabDateOptions' +import SideBarTabTextOptions from './SideBarTabTextOptions' +import SideBarTabComments from './SideBarTabComments' +import SideBarTabShare from './SideBarTabShare' +import { mapState } from 'vuex' + +export default { + name: 'SideBar', + components: { + SideBarTabConfiguration, + SideBarTabComments, + SideBarTabDateOptions, + SideBarTabTextOptions, + SideBarTabShare, + AppSidebar, + AppSidebarTab + }, + + data() { + return { + initialTab: 'comments' + } + }, + + computed: { + ...mapState({ + event: state => state.event, + acl: state => state.acl + }) + } + +} + +</script> + +<style scoped lang="scss"> + + ul { + & > li { + margin-bottom: 30px; + & > .comment-item { + display: flex; + align-items: center; + + & > .date { + right: 0; + top: 5px; + opacity: 0.5; + } + } + & > .message { + margin-left: 44px; + flex: 1 1; + } + } + } +</style> diff --git a/src/js/components/SideBar/SideBarTabComments.vue b/src/js/components/SideBar/SideBarTabComments.vue new file mode 100644 index 00000000..1d4c9fb4 --- /dev/null +++ b/src/js/components/SideBar/SideBarTabComments.vue @@ -0,0 +1,98 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div> + <CommentAdd /> + <transition-group v-if="countComments" name="fade" class="comments" + tag="ul"> + <li v-for="(comment) in sortedComments" :key="comment.id"> + <div class="comment-item"> + <user-div :user-id="comment.userId" /> + <div class="date"> + {{ realtiveDate(comment.date) }} + </div> + </div> + <div class="message wordwrap comment-content"> + {{ comment.comment }} + </div> + </li> + </transition-group> + <div v-else class="emptycontent"> + <div class="icon-comment" /> + <p> {{ t('polls', 'No comments yet. Be the first.') }}</p> + </div> + </div> +</template> + +<script> +import moment from 'moment' +import CommentAdd from './CommentAdd' +import { mapState, mapGetters } from 'vuex' + +export default { + name: 'SideBarTabComments', + components: { + CommentAdd + }, + + computed: { + ...mapState({ + comments: state => state.comments + }), + ...mapGetters([ + 'countComments', + 'sortedComments' + ]) + }, + + methods: { + realtiveDate(date) { + return t('core', moment.utc(date).fromNow()) + } + + } +} +</script> + +<style scoped lang="scss"> + + ul { + & > li { + margin-bottom: 30px; + & > .comment-item { + display: flex; + align-items: center; + + & > .date { + right: 0; + top: 5px; + opacity: 0.5; + } + } + & > .message { + margin-left: 44px; + flex: 1 1; + } + } + } +</style> diff --git a/src/js/components/SideBar/SideBarTabConfiguration.vue b/src/js/components/SideBar/SideBarTabConfiguration.vue new file mode 100644 index 00000000..1e135af1 --- /dev/null +++ b/src/js/components/SideBar/SideBarTabConfiguration.vue @@ -0,0 +1,344 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div> + <div class="configBox"> + <label v-if="writingPoll" class="icon-loading-small title"> + {{ t('polls', 'Saving') }} + </label> + <label v-else class="icon-checkmark title"> + {{ t('polls', 'Saved') }} + </label> + </div> + + <div v-if="acl.allowEdit" class="configBox"> + <label class="icon-sound title"> + {{ t('polls', 'Title') }} + </label> + <input v-model="eventTitle" :class="{ error: titleEmpty }" type="text"> + </div> + + <div v-if="acl.allowEdit" class="configBox"> + <label class="icon-edit title"> + {{ t('polls', 'Description') }} + </label> + <textarea v-model="eventDescription" /> + <!-- <textarea v-if="acl.allowEdit" :value="event.description" @input="updateDescription" /> --> + </div> + + <div class="configBox"> + <label class="title icon-category-customization"> + {{ t('polls', 'Poll configurations') }} + </label> + + <input id="allowMaybe" + v-model="eventAllowMaybe" + :disabled="!acl.allowEdit" + type="checkbox" + class="checkbox"> + <label for="allowMaybe" class="title"> + {{ t('polls', 'Allow "maybe" vote') }} + </label> + + <input id="anonymous" v-model="eventIsAnonymous" + :disabled="!acl.allowEdit" + type="checkbox" + class="checkbox"> + <label for="anonymous" class="title"> + {{ t('polls', 'Anonymous poll') }} + </label> + + <input v-show="event.isAnonymous" + id="trueAnonymous" + v-model="eventFullAnonymous" + :disabled="!acl.allowEdit" + type="checkbox" + class="checkbox"> + <label v-show="event.isAnonymous" class="title" for="trueAnonymous"> + {{ t('polls', 'Hide user names for admin') }} + </label> + + <input id="expiration" + v-model="eventExpiration" + :disabled="!acl.allowEdit" + type="checkbox" + class="checkbox"> + <label class="title" for="expirtion"> + {{ t('polls', 'Expires') }} + </label> + + <date-picker v-show="event.expire" + v-model="eventExpiration" + v-bind="expirationDatePicker" + :disabled="!acl.allowEdit" + :time-picker-options="{ start: '00:00', step: '00:05', end: '23:55' }" + style="width:170px" /> + </div> + + <div class="configBox"> + <label class="title icon-category-auth"> + {{ t('polls', 'Access') }} + </label> + + <input id="hidden" + v-model="eventAccess" + :disabled="!acl.allowEdit" + type="radio" + value="hidden" + class="radio"> + <label for="hidden" class="title"> + <div class="title icon-category-security" /> + <span>{{ t('polls', 'Hidden to other users') }}</span> + </label> + + <input id="public" + v-model="eventAccess" + :disabled="!acl.allowEdit" + type="radio" + value="public" + class="radio"> + <label for="public" class="title"> + <div class="title icon-link" /> + <span>{{ t('polls', 'Visible to other users') }}</span> + </label> + </div> + + <button class="button btn primary" @click="$emit('deletePoll')"> + <span>{{ t('polls', 'Delete this poll') }}</span> + </button> + </div> +</template> + +<script> +import debounce from 'lodash/debounce' +import { mapState, mapMutations, mapActions, mapGetters } from 'vuex' + +export default { + name: 'SideBarTab', + + data() { + return { + writingPoll: false, + sidebar: false, + titleEmpty: false + } + }, + + computed: { + ...mapState({ + event: state => state.event, + acl: state => state.acl + }), + + ...mapGetters([ + 'languageCodeShort' + ]), + + // Add bindings + eventDescription: { + get() { + return this.event.description + }, + set(value) { + this.writeValueDebounced({ 'description': value }) + } + }, + + eventTitle: { + get() { + return this.event.title + }, + set(value) { + this.writeValueDebounced({ 'title': value }) + } + }, + + eventAccess: { + get() { + return this.event.access + }, + set(value) { + this.writeValue({ 'access': value }) + } + }, + + eventExpiration: { + get() { + return this.event.expire + }, + set(value) { + this.writeValue({ 'expire': value }) + } + }, + + eventFullAnonymous: { + get() { + return this.event.fullAnonymous + }, + set(value) { + this.writeValue({ 'fullAnonymous': value }) + } + }, + + eventIsAnonymous: { + get() { + return this.event.isAnonymous + }, + set(value) { + this.writeValue({ 'isAnonymous': value }) + } + }, + + eventAllowMaybe: { + get() { + return this.event.allowMaybe + }, + set(value) { + this.writeValue({ 'allowMaybe': value }) + } + }, + + // eventExpiration: { + // get() { + // return this.$store.state.event.expiration + // }, + // set(value) { + // this.writeValue({ 'expiration': value }) + // } + // }, + + expirationDatePicker() { + return { + editable: true, + minuteStep: 1, + type: 'datetime', + format: this.dateTimeFormat, + lang: this.langShort, + placeholder: t('polls', 'Expiration date'), + timePickerOptions: { + start: '00:00', + step: '00:30', + end: '23:30' + } + } + }, + + optionDatePicker() { + return { + editable: false, + minuteStep: 1, + type: 'datetime', + format: this.dateTimeFormat, + lang: this.languageCodeShort, + placeholder: t('polls', 'Click to add a date'), + timePickerOptions: { + start: '00:00', + step: '00:30', + end: '23:30' + } + } + }, + + protect: function() { + return this.poll.mode === 'vote' + }, + + saveButtonTitle: function() { + if (this.writingPoll) { + return t('polls', 'Writing poll') + } else if (this.acl.allowEdit) { + return t('polls', 'Update poll') + } else { + return t('polls', 'Create new poll') + } + } + }, + methods: { + + ...mapMutations([ 'setEventProperty' ]), + ...mapActions([ 'writeEventPromise' ]), + + writeValueDebounced: debounce(function(e) { + this.writeValue(e) + }, 1500), + + writeValue(e) { + this.$store.commit('setEventProperty', e) + this.writingPoll = true + this.writePoll() + }, + + writePoll() { + if (this.titleEmpty) { + OC.Notification.showTemporary(t('polls', 'Title must not be empty!'), { type: 'success' }) + } else { + this.writeEventPromise() + this.writingPoll = false + OC.Notification.showTemporary(t('polls', '%n successfully saved', 1, this.event.title), { type: 'success' }) + } + }, + + write() { + if (this.acl.allowEdit) { + this.writePoll() + } + + } + } +} +</script> + +<style lang="scss"> + .configBox { + display: flex; + flex-direction: column; + padding: 8px; + & > * { + padding-left: 21px; + } + + & > input { + margin-left: 24px; + width: auto; + + } + + & > textarea { + margin-left: 24px; + width: auto; + padding: 7px 6px; + } + + & > .title { + display: flex; + background-position: 0 2px; + padding-left: 24px; + opacity: 0.7; + font-weight: bold; + margin-bottom: 4px; + & > span { + padding-left: 4px; + } + } + } +</style> diff --git a/src/js/components/SideBar/SideBarTabDateOptions.vue b/src/js/components/SideBar/SideBarTabDateOptions.vue new file mode 100644 index 00000000..c99b129b --- /dev/null +++ b/src/js/components/SideBar/SideBarTabDateOptions.vue @@ -0,0 +1,173 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div> + <div class="configBox"> + <label class="title icon-calendar"> + {{ t('polls', 'Add a date option') }} + </label> + <DatePicker v-bind="optionDatePicker" style="width:100%" confirm + @change="addOption($event)" /> + </div> + + <div class="configBox"> + <label class="title icon-history"> + {{ t('polls', 'Shift all date options') }} + </label> + <div> + <div class="selectUnit"> + <input v-model="move.step"> + <Multiselect v-model="move.unit" :options="move.units" /> + </div> + </div> + <div> + <button class="button btn primary" @click="shiftDates(move)"> + <span>{{ t('polls', 'Shift') }}</span> + </button> + </div> + </div> + + <ul class="configBox poll-table"> + <label class="title icon-calendar"> + {{ t('polls', 'Available Options') }} + </label> + <DatePollItem v-for="(option) in sortedOptions" + :key="option.id" + :option="option" + @remove="removeOption(option)" /> + </ul> + </div> +</template> + +<script> +import { Multiselect } from '@nextcloud/vue' +import moment from 'moment' +import { mapGetters, mapState } from 'vuex' +import DatePollItem from '../Create/CreateDateItem' + +export default { + name: 'SideBarTabDateOptions', + + components: { + Multiselect, + DatePollItem + + }, + + data() { + return { + move: { + step: 1, + unit: 'week', + units: ['minute', 'hour', 'day', 'week', 'month', 'year'] + } + } + }, + + computed: { + ...mapState({ + options: state => state.options + }), + + ...mapGetters([ 'languageCodeShort', 'sortedOptions' ]), + + optionDatePicker() { + return { + editable: false, + minuteStep: 1, + type: 'datetime', + format: this.dateTimeFormat, + lang: this.languageCodeShort, + placeholder: t('polls', 'Click to add a date'), + timePickerOptions: { + start: '00:00', + step: '00:30', + end: '23:30' + } + } + } + }, + + methods: { + + addOption(pollOptionText) { + this.$store.dispatch('addOptionAsync', { pollOptionText: pollOptionText }) + }, + + shiftDates(payload) { + let store = this.$store + this.options.list.forEach(function(existingOption) { + let option = Object.assign({}, existingOption) + option.pollOptionText = moment(option.pollOptionText).add(payload.step, payload.unit).format('YYYY-MM-DD HH:mm:ss') + option.timestamp = moment.utc(option.pollOptionText).unix() + store.dispatch('updateOptionAsync', { option: option }) + }) + }, + + removeOption(option) { + this.$store.dispatch('removeOptionAsync', { option: option }) + } + + } + +} +</script> + +<style lang="scss"> + .configBox { + display: flex; + flex-direction: column; + padding: 8px; + & > * { + padding-left: 21px; + } + + & > input { + margin-left: 24px; + width: auto; + + } + + & > textarea { + margin-left: 24px; + width: auto; + padding: 7px 6px; + } + + & > .title { + display: flex; + background-position: 0 2px; + padding-left: 24px; + opacity: 0.7; + font-weight: bold; + margin-bottom: 4px; + & > span { + padding-left: 4px; + } + } + &.poll-table > li { + border-bottom-color: rgb(72, 72, 72); + margin-left: 18px; + } + } +</style> diff --git a/src/js/components/SideBar/SideBarTabInformation.vue b/src/js/components/SideBar/SideBarTabInformation.vue new file mode 100644 index 00000000..edffcaaf --- /dev/null +++ b/src/js/components/SideBar/SideBarTabInformation.vue @@ -0,0 +1,58 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div> + <user-div :user-id="event.owner" :description="t('polls', 'Owner')" /> + <div>{{ accessType }}</div> + <h3> {{ t('polls', 'Created') }} </h3> + <div>{{ timeSpanCreated }}</div> + <h3> {{ t('polls', 'Expires') }} </h3> + <div>{{ timeSpanExpiration }}</div> + <div>{{ countCommentsHint }}</div> + </div> +</template> + +<script> +import { mapState, mapGetters } from 'vuex' + +export default { + name: 'SideBarTabInformationTab', + computed: { + ...mapState({ + event: state => state.event + }), + + ...mapGetters([ + 'accessType', + 'countComments', + 'timeSpanCreated', + 'timeSpanExpiration' + ]), + + countCommentsHint: function() { + return n('polls', 'There is %n comment', 'There are %n comments', this.countComments) + } + } +} + +</script> diff --git a/src/js/components/SideBar/SideBarTabShare.vue b/src/js/components/SideBar/SideBarTabShare.vue new file mode 100644 index 00000000..080be7db --- /dev/null +++ b/src/js/components/SideBar/SideBarTabShare.vue @@ -0,0 +1,211 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div> + <h3>{{ t('polls','Invitations') }}</h3> + <TransitionGroup :css="false" tag="ul" class="shared-list"> + <li v-for="(share) in invitationShares" :key="share.id"> + <UserDiv :user-id="resolveShareUser(share)" :type="share.type" :icon="true" /> + <div class="options"> + <a class="icon icon-clippy" @click="copyLink( { url: OC.generateUrl('apps/polls/s/') + share.token } )" /> + <a class="icon icon-delete svg delete-poll" @click="removeShare(share)" /> + </div> + </li> + </TransitionGroup> + + <Multiselect id="ajax" + :options="users" + :multiple="false" + :user-select="true" + :tag-width="80" + :clear-on-select="false" + :preserve-search="true" + :options-limit="20" + :loading="isLoading" + :internal-search="false" + :searchable="true" + :preselect-first="true" + :placeholder="placeholder" + label="displayName" + track-by="user" + @select="addShare" + @search-change="loadUsersAsync"> + <template slot="selection" slot-scope="{ values, search, isOpen }"> + <span v-if="values.length && !isOpen" class="multiselect__single"> + {{ values.length }} users selected + </span> + </template> + </Multiselect> + + <h3>{{ t('polls','Public shares') }}</h3> + <TransitionGroup :css="false" tag="ul" class="shared-list"> + <li v-for="(share) in publicShares" :key="share.id"> + <div class="user-row user"> + <div class="avatar icon-public" /> + <div class="user-name"> + {{ t('polls', 'Share Link') }} + </div> + </div> + <div class="options"> + <a class="icon icon-clippy" @click="copyLink( { url: OC.generateUrl('apps/polls/s/') + share.token } )" /> + <a class="icon icon-delete" @click="removeShare(share)" /> + </div> + </li> + </TransitionGroup> + <div class="user-row user" @click="addShare({type: 'public', user: ''})"> + <div class="avatar icon-add" /> + <div class="user-name"> + {{ t('polls', 'Add a public link') }} + </div> + </div> + </div> +</template> + +<script> +import { Multiselect } from '@nextcloud/vue' +import { mapGetters } from 'vuex' + +export default { + name: 'SideBarTabShare', + + components: { + Multiselect + }, + + data() { + return { + users: [], + invitations: [], + invitation: {}, + isLoading: false, + siteUsersListOptions: { + getUsers: true, + getGroups: true, + query: '' + } + } + }, + + computed: { + ...mapGetters([ + 'countShares', + 'sortedShares', + 'invitationShares', + 'publicShares' + ]) + }, + + methods: { + loadUsersAsync(query) { + this.isLoading = false + this.siteUsersListOptions.query = query + this.$http.post(OC.generateUrl('apps/polls/get/siteusers'), this.siteUsersListOptions) + .then((response) => { + this.users = response.data.siteusers + this.isLoading = false + }, (error) => { + console.error(error.response) + }) + }, + + copyLink(payload) { + this.$copyText(window.location.origin + payload.url).then( + function(e) { + OC.Notification.showTemporary(t('polls', 'Link copied to clipboard'), { type: 'success' }) + }, + function(e) { + OC.Notification.showTemporary(t('polls', 'Error while copying link to clipboard'), { type: 'error' }) + } + ) + }, + + resolveShareUser(share) { + if (share.userId !== '' && share.userId !== null) { + return share.userId + } else if (share.type === 'mail') { + return share.userEmail + } else { + return t('polls', 'Unknown user') + } + + }, + + removeShare(share) { + this.$store.dispatch('removeShareAsync', { share: share }) + }, + + addShare(payload) { + this.$store.dispatch('writeSharePromise', { + 'share': { + 'type': payload.type, + 'userId': payload.user, + 'pollId': '0', + 'userEmail': '', + 'token': '' + } + }) + // .then(response => { + // OC.Notification.showTemporary(t('polls', 'You added %n.', 1, payload.user), { type: 'success' }) + // }) + .catch(error => { + console.error('Error while adding share comment - Error: ', error) + OC.Notification.showTemporary(t('polls', 'Error while adding share'), { type: 'error' }) + }) + } + } +} +</script> + +<style lang="scss"> + .shared-list { + display: flex; + flex-wrap: wrap; + flex-direction: column; + justify-content: flex-start; + padding-top: 8px; + + > li { + display: flex; + align-items: stretch; + margin: 4px 0; + } + } + + .options { + display: flex; + + .icon:not(.hidden) { + padding: 14px; + height: 44px; + width: 44px; + opacity: .5; + display: block; + cursor: pointer; + } + } + + .multiselect { + width: 100% !important; + max-width: 100% !important; + } +</style> diff --git a/src/js/components/SideBar/SideBarTabTextOptions.vue b/src/js/components/SideBar/SideBarTabTextOptions.vue new file mode 100644 index 00000000..002d5d62 --- /dev/null +++ b/src/js/components/SideBar/SideBarTabTextOptions.vue @@ -0,0 +1,164 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div> + <div class="configBox"> + <label class="title icon-toggle-filelist"> + {{ t('polls', 'Add a new text option') }} + </label> + <input v-model="newPollText" :placeholder=" t('polls', 'Enter option text and press Enter') " @keyup.enter="addOption(newPollText)"> + </div> + + <ul class="configBox poll-table"> + <label class="title icon-calendar"> + {{ t('polls', 'Available Options') }} + </label> + <TextPollItem v-for="(option) in sortedOptions" :key="option.id" :option="option" + @remove="removeOption(option)" /> + </ul> + </div> +</template> + +<script> +import { mapGetters, mapState } from 'vuex' +import TextPollItem from '../Create/TextPollItem' +export default { + name: 'SideBarTabTextOptions', + + components: { + TextPollItem + }, + + data() { + return { + newPollText: '' + } + }, + + computed: { + ...mapState({ + options: state => state.options + }), + + ...mapGetters(['languageCodeShort', 'sortedOptions']) + }, + + methods: { + + addOption(pollOptionText) { + if (pollOptionText !== '') { + this.$store.dispatch('addOptionAsync', { + pollOptionText: pollOptionText + }) + this.newPollText = '' + } + }, + + removeOption(option) { + this.$store.dispatch('removeOptionAsync', { + option: option + }) + } + + } + +} +</script> + +<style lang="scss"> +.configBox { + display: flex; + flex-direction: column; + padding: 8px; + & > * { + padding-left: 21px; + } + + & > input { + margin-left: 24px; + width: auto; + + } + + & > textarea { + margin-left: 24px; + width: auto; + padding: 7px 6px; + } + + & > .title { + display: flex; + background-position: 0 2px; + padding-left: 24px; + opacity: 0.7; + font-weight: bold; + margin-bottom: 4px; + & > span { + padding-left: 4px; + } + } + + &.poll-table > li { + border-bottom-color: rgb(72, 72, 72); + margin-left: 18px; + } + +} + +.poll-table { + > li { + display: flex; + align-items: center; + padding-left: 8px; + padding-right: 8px; + line-height: 2em; + min-height: 4em; + border-bottom: 1px solid var(--color-border); + overflow: hidden; + white-space: nowrap; + + &:active, + &:hover { + transition: var(--background-dark) 0.3s ease; + background-color: var(--color-background-dark); //$hover-color; + } + + > div { + display: flex; + flex: 1; + font-size: 1.2em; + opacity: 0.7; + white-space: normal; + padding-right: 4px; + &.avatar { + flex: 0; + } + } + + > div:nth-last-child(1) { + justify-content: center; + flex: 0 0; + } + } +} +</style> diff --git a/src/js/components/VoteTable/VoteHeader.vue b/src/js/components/VoteTable/VoteHeader.vue new file mode 100644 index 00000000..d8eb7fc7 --- /dev/null +++ b/src/js/components/VoteTable/VoteHeader.vue @@ -0,0 +1,77 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div class="voteHeader"> + <h2> + {{ event.title }} + <span v-if="expired" class="label error">{{ t('polls', 'Expired since %n', 1, timeSpanExpiration) }}</span> + <span v-if="!expired && isExpirationSet" class="label success">{{ t('polls', 'Place your votes until %n', 1, event.expire) }}</span> + <span v-if="!isExpirationSet" class="label success">{{ t('polls', 'No expiration date set') }}</span> + </h2> + <h3> + {{ event.description }} + </h3> + </div> +</template> + +<script> +import { mapState, mapGetters } from 'vuex' + +export default { + name: 'VoteHeader', + + data() { + return { + voteSaved: false, + delay: 50, + newName: '' + } + }, + + computed: { + ...mapState({ + event: state => state.event + }), + + ...mapGetters([ + 'isExpirationSet', + 'expired', + 'timeSpanExpiration' + ]) + + }, + + methods: { + indicateVoteSaved() { + this.voteSaved = true + window.setTimeout(this.timer, this.delay) + } + } +} +</script> + +<style lang="scss" scoped> + .voteHeader { + margin: 8px 24px; + } +</style> diff --git a/src/js/components/VoteTable/VoteHeaderPublic.vue b/src/js/components/VoteTable/VoteHeaderPublic.vue new file mode 100644 index 00000000..4142fbbd --- /dev/null +++ b/src/js/components/VoteTable/VoteHeaderPublic.vue @@ -0,0 +1,221 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div class="voteHeader"> + <div v-if="!isValidUser" class="getUsername"> + <!-- <label> + {{ t('polls', 'Enter a valid username, to participate in this poll.') }} + </label> --> + + <form v-if="!redirecting"> + <input v-model="userName" :class="{ error: (!isValidName && userName.length > 0), success: isValidName }" type="text" + :placeholder="t('polls', 'Enter a valid username with at least 3 Characters')"> + <input v-show="isValidName && !checkingUserName" class="icon-confirm" :class="{ error: !isValidName, success: isValidName }" + @click="writeUserName"> + <span v-show="checkingUserName" class="icon-loading-small" /> + <!-- <span v-if="!isValidName" class="error"> {{ invalidUserNameMessage }} </span> --> + </form> + <div v-else> + <span>{{ t('polls', 'You will be redirected to your personal share.') }}</span> + <span> + {{ t('polls', 'If you are not redirected to your poll click this link:') }} + <router-link :to="{ name: 'publicVote', params: { token: token }}"> + Link + </router-link> + </span> + </div> + </div> + <div v-if="displayLink" class="personal-link"> + {{ t('polls', 'Your personal link to this poll: %n', 1, personalLink) }} + <a class="icon icon-clippy" @click="copyLink( { url: OC.generateUrl($route.path) } )" /> + </div> + </div> +</template> + +<script> +import debounce from 'lodash/debounce' +import axios from '@nextcloud/axios' +import { mapState } from 'vuex' + +export default { + name: 'VoteHeaderPublic', + + data() { + return { + userName: '', + token: '', + checkingUserName: false, + redirecting: false, + isValidName: false, + newName: '' + } + }, + + computed: { + ...mapState({ + event: state => state.event, + acl: state => state.acl + }), + + personalLink() { + return location.protocol.concat('//', window.location.hostname, OC.generateUrl(this.$route.path)) + }, + + displayLink() { + return (this.acl.userId !== '' && this.acl.userId !== null && this.acl.foundByToken) + }, + + isValidUser() { + return (this.acl.userId !== '' && this.acl.userId !== null) + } + + }, + + watch: { + userName: function() { + if (this.userName.length > 2) { + this.isValidName = this.validatePublicUsername() + } else { + this.invalidUserNameMessage = t('polls', 'Please use at least 3 characters for your user name!') + this.isValidName = false + } + } + }, + + methods: { + copyLink(payload) { + this.$copyText(window.location.origin + payload.url).then( + function(e) { + OC.Notification.showTemporary(t('polls', 'Link copied to clipboard'), { type: 'success' }) + }, + function(e) { + OC.Notification.showTemporary(t('polls', 'Error while copying link to clipboard'), { type: 'error' }) + } + ) + }, + validatePublicUsername: debounce(function() { + if (this.userName.length > 2) { + this.checkingUserName = true + return axios.post(OC.generateUrl('apps/polls/check/username'), { pollId: this.event.id, userName: this.userName, token: this.$route.params.token }) + .then((response) => { + this.checkingUserName = false + this.isValidName = true + this.invalidUserNameMessage = 'User name is OK.' + return true + }) + .catch(() => { + this.checkingUserName = false + this.isValidName = false + this.invalidUserNameMessage = t('polls', 'This user name can not be choosed.') + return false + }) + } else { + this.checkingUserName = false + this.isValidName = false + this.invalidUserNameMessage = t('polls', 'Please use at least 3 characters for your user name!') + return false + } + }, 500), + + writeUserName() { + if (this.validatePublicUsername()) { + this.$store.dispatch('addShareFromUser', { token: this.$route.params.token, userName: this.userName }) + .then((response) => { + this.token = response.token + this.redirecting = true + this.$router.replace({ name: 'publicVote', params: { 'token': response.token } }) + }) + .catch(() => { + OC.Notification.showTemporary(t('polls', 'Error saving user name"', 1, event.title), { type: 'error' }) + }) + } + } + } +} +</script> + +<style lang="scss" scoped> + .voteHeader { + margin: 8px 24px; + } + + .personal-link { + display: flex; + padding: 4px 12px; + margin: 0 12px 0 24px; + border: 2px solid var(--color-success); + background-color: #d6fdda !important; + border-radius: var(--border-radius); + font-size: 1.2em; + opacity: 0.8; + .icon { + margin: 0 12px; + } + } + + .getUsername { + & > label { + margin-right: 12px; + } + + margin: 0 12px 12px 24px; + border:2px solid var(--color-border-dark); + font-size: 1.2em; + padding: 0 12px 0 12px; + display: flex; + align-items: center; + border-radius: var(--border-radius); + background-color: var(--color-background-dark); + flex-wrap: wrap; + + form, div { + flex: 1; + display: flex; + + } + input { + flex: 1; + } + + .icon-loading-small { + position: relative; + right: 24px; + top: 0px; + } + + input[type="text"] + .icon-confirm, input[type="text"] + .icon-loading-small { + flex: 0; + margin-left: -8px !important; + border-left-color: transparent !important; + border-radius: 0 var(--border-radius) var(--border-radius) 0 !important; + background-clip: padding-box; + opacity: 1; + height: 34px; + width: 34px; + padding: 7px 20px; + cursor: pointer; + margin-right: 0; + } + } + +</style> diff --git a/src/js/components/VoteTable/VoteTable.vue b/src/js/components/VoteTable/VoteTable.vue new file mode 100644 index 00000000..c5c8a48b --- /dev/null +++ b/src/js/components/VoteTable/VoteTable.vue @@ -0,0 +1,210 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template lang="html"> + <div class="vote-table"> + <div class="header"> + <div class="sticky" /> + + <div v-if="noOptions" class="noOptions"> + <h2> {{ t('polls', 'there are no vote Options') }} </h2> + </div> + + <VoteTableHeader v-for="(option) in sortedOptions" + :key="option.id" + :option="option" + :poll-type="event.type" /> + </div> + + <div v-for="(participant) in participants" :key="participant" :class="{currentuser: (participant === currentUser) }"> + <UserDiv :key="participant" + class="sticky" + :class="{currentuser: (participant === currentUser) }" + :user-id="participant" /> + <VoteTableItem v-for="(option) in sortedOptions" + :key="option.id" + :user-id="participant" + :option="option" + @voteClick="setVote(option, participant)" /> + </div> + </div> +</template> + +<script> +import VoteTableItem from './VoteTableItem' +import VoteTableHeader from './VoteTableHeader' +import { mapState, mapGetters } from 'vuex' + +export default { + name: 'VoteTable', + components: { + VoteTableHeader, + VoteTableItem + }, + + computed: { + ...mapState({ + event: state => state.event, + acl: state => state.acl + }), + + ...mapGetters([ + 'sortedOptions', + 'participants' + ]), + + currentUser() { + return this.acl.userId + }, + + noOptions() { + return (this.sortedOptions.length === 0) + } + }, + + methods: { + setVote(option, participant) { + let nextAnswer = this.$store.getters.getNextAnswer({ + option: option, + userId: participant + }) + this.$store + .dispatch('setVoteAsync', { + option: option, + userId: participant, + setTo: nextAnswer + }) + .then(() => { + // this.$emit('voteSaved') + }) + } + } +} +</script> + +<style lang="scss" scoped> + .user-row.sticky, + .header > .sticky { + position: sticky; + left: 0; + background-color: var(--color-main-background); + width: 170px; + flex: 0 0 auto; + } + .header { + height: 150px; + } + .user { + height: 44px; + padding: 0 17px; + } + .vote-table { + display: flex; + flex: 0; + flex-direction: column; + justify-content: flex-start; + overflow: scroll; + + & > div { + display: flex; + flex: 1; + border-bottom: 1px solid var(--color-border-dark); + order: 3; + justify-content: space-between; + min-width: max-content; + + & > div { + width: 84px; + min-width: 84px; + flex: 1; + margin: 2px; + } + + & > .vote-header { + flex: 1; + } + + &.header { + order: 1; + } + + &.currentuser { + order: 2; + } + } + + .vote-row { + display: flex; + justify-content: space-around; + flex: 1; + align-items: center; + } + } + + @media (max-width: (480px)) { + .vote-table { + flex: 1 0; + flex-direction: row; + min-width: 300px; + + &> div { + display: none; + &> div { + width: unset; + margin: 0; + + } + // &.currentuser { + // display: flex; + // > .user-row.currentuser { + // display: none; + // } + // } + } + + &> .currentuser { + display: flex; + flex-direction: column; + &> .user-row { + display: none; + } + } + + &> .header, { + height: initial; + padding-left: initial; + display: flex; + flex-direction: column; + flex: 3 1; + justify-content: space-around; + align-items: stretch; + &> .vote-header { + display: flex; + flex-direction: row; + &> .counter { + align-items: baseline; + } + } + } + } + } +</style> diff --git a/src/js/components/VoteTable/VoteTableHeader.vue b/src/js/components/VoteTable/VoteTableHeader.vue new file mode 100644 index 00000000..28634cb8 --- /dev/null +++ b/src/js/components/VoteTable/VoteTableHeader.vue @@ -0,0 +1,291 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div class="vote-header" :class=" { winner: isWinner }"> + <div v-if="textPoll" class="text-box"> + {{ option.pollOptionText }} + </div> + + <div v-if="datePoll" v-tooltip.auto="localFullDate" class="date-box"> + <div class="month"> + {{ monthY }} + </div> + <div class="day"> + {{ day }} + </div> + <div class="dow"> + {{ dow }} + </div> + <div class="time"> + {{ time }} + </div> + </div> + + <div class="counter"> + <div class="yes"> + <span> {{ yesVotes }} </span> + </div> + <div v-if="event.allowMaybe" class="maybe"> + <span> {{ maybeVotes }} </span> + </div> + </div> + </div> +</template> + +<script> +import moment from 'moment' +import { mapState, mapGetters } from 'vuex' + +export default { + name: 'VoteTableHeader', + + props: { + option: { + type: Object, + default: undefined + }, + pollType: { + type: String, + default: undefined + } + }, + + data() { + return { + openedMenu: false, + hostName: this.$route.query.page + } + + }, + + computed: { + ...mapState({ + event: state => state.event, + votes: state => state.votes.list + }), + ...mapGetters([ + 'votesRank', + 'winnerCombo' + ]), + + votesranked() { + let pollOptionText = this.option.pollOptionText + return this.votesRank.find(rank => { + return rank.pollOptionText === pollOptionText + }) + }, + + yesVotes() { + let pollOptionText = this.option.pollOptionText + return this.votesRank.find(rank => { + return rank.pollOptionText === pollOptionText + }).yes + }, + + noVotes() { + let pollOptionText = this.option.pollOptionText + return this.votesRank.find(rank => { + return rank.pollOptionText === pollOptionText + }).no + }, + + maybeVotes() { + let pollOptionText = this.option.pollOptionText + return this.votesRank.find(rank => { + return rank.pollOptionText === pollOptionText + }).maybe + }, + + isWinner() { + let pollOptionText = this.option.pollOptionText + return ( + this.votesRank.find(rank => { + return rank.pollOptionText === pollOptionText + }).yes === this.winnerCombo.yes + + && (this.votesRank.find(rank => { + return rank.pollOptionText === pollOptionText + }).yes + this.votesRank.find(rank => { + return rank.pollOptionText === pollOptionText + }).maybe > 0) + + && this.winnerCombo.maybe === this.votesRank.find(rank => { + return rank.pollOptionText === pollOptionText + }).maybe + ) + }, + + datePoll() { + return (this.event.type === 'datePoll') + }, + + textPoll() { + return (this.event.type === 'textPoll') + }, + + localFullDate() { + return moment(this.option.timestamp * 1000).format('llll') + }, + + day() { + return moment(this.option.timestamp * 1000).format('Do') + }, + + dow() { + return moment(this.option.timestamp * 1000).format('ddd') + }, + + month() { + return moment(this.option.timestamp * 1000).format('MMM') + }, + + monthY() { + return this.month + " '" + moment(this.option.timestamp * 1000).format('YY') + }, + + year() { + return moment(this.option.timestamp * 1000).format('YYYY') + }, + + time() { + return moment(this.option.timestamp * 1000).format('LT') + }, + + localFullDateT() { + return moment(this.option.pollOptionText).format('llll') + }, + + dayT() { + return moment(this.option.pollOptionText).format('Do') + }, + + dowT() { + return moment(this.option.pollOptionText).format('ddd') + }, + + monthT() { + return moment(this.option.pollOptionText).format('MMM') + }, + + yearT() { + return moment(this.option.pollOptionText).format('YYYY') + }, + + timeT() { + return moment(this.option.pollOptionText).format('LT') + } + } +} +</script> + +<style lang="scss" scoped> + +.vote-header { + display: flex; + flex-direction: column; + &.winner { + font-weight: bold; + color: #49bc49; + } +} + +.counter { + flex: 0; + display: flex; + justify-content: center; + font-size: 18px; + height: 88px; + padding: 14px 4px; + + &> * { + background-position: 0px 2px; + padding-left: 23px; + background-repeat: no-repeat; + background-size: contain; + margin-right: 8px; + } + + .yes { + color: #49bc49; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTYiIHdpZHRoPSIxNiIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxwYXRoIGQ9Im0yLjM1IDcuMyA0IDRsNy4zLTcuMyIgc3Ryb2tlPSIjNDliYzQ5IiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9Im5vbmUiLz48L3N2Zz4K); + } + .no { + color: #f45573; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTYiIHdpZHRoPSIxNiIgdmVyc2lvbj0iMS4xIiB2aWV3Ym94PSIwIDAgMTYgMTYiPjxwYXRoIGQ9Im0xNCAxMi4zLTEuNyAxLjctNC4zLTQuMy00LjMgNC4zLTEuNy0xLjcgNC4zLTQuMy00LjMtNC4zIDEuNy0xLjcgNC4zIDQuMyA0LjMtNC4zIDEuNyAxLjctNC4zIDQuM3oiIGZpbGw9IiNmNDU1NzMiLz48L3N2Zz4K); + } + .maybe { + color: #ffc107; + background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgaWQ9InN2ZzQiCiAgIHZlcnNpb249IjEuMSIKICAgd2lkdGg9IjE2IgogICBoZWlnaHQ9IjE2IgogICBzb2RpcG9kaTpkb2NuYW1lPSJtYXliZS12b3RlLXZhcmlhbnQuc3ZnIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjkyLjIgKDVjM2U4MGQsIDIwMTctMDgtMDYpIj4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IgogICAgIGJvcmRlcm9wYWNpdHk9IjEiCiAgICAgb2JqZWN0dG9sZXJhbmNlPSIxMCIKICAgICBncmlkdG9sZXJhbmNlPSIxMCIKICAgICBndWlkZXRvbGVyYW5jZT0iMTAiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjE5MjAiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iMTAxNyIKICAgICBpZD0ibmFtZWR2aWV3NiIKICAgICBzaG93Z3JpZD0iZmFsc2UiCiAgICAgaW5rc2NhcGU6em9vbT0iMTQuNzUiCiAgICAgaW5rc2NhcGU6Y3g9IjgiCiAgICAgaW5rc2NhcGU6Y3k9IjE0Ljg2NTIwMSIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iLTgiCiAgICAgaW5rc2NhcGU6d2luZG93LXk9Ii04IgogICAgIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjEiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ic3ZnNCI+CiAgICA8aW5rc2NhcGU6Z3JpZAogICAgICAgdHlwZT0ieHlncmlkIgogICAgICAgaWQ9ImdyaWQ4MzYiIC8+CiAgPC9zb2RpcG9kaTpuYW1lZHZpZXc+CiAgPG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhMTAiPgogICAgPHJkZjpSREY+CiAgICAgIDxjYzpXb3JrCiAgICAgICAgIHJkZjphYm91dD0iIj4KICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4KICAgICAgICA8ZGM6dHlwZQogICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+CiAgICAgICAgPGRjOnRpdGxlPjwvZGM6dGl0bGU+CiAgICAgIDwvY2M6V29yaz4KICAgIDwvcmRmOlJERj4KICA8L21ldGFkYXRhPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM4IiAvPgogIDx0ZXh0CiAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc2l6ZToxNS4wMDY0OTA3MXB4O2xpbmUtaGVpZ2h0OjEuMjU7Zm9udC1mYW1pbHk6c2Fucy1zZXJpZjtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiNmZmMxMDc7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuMDIzMTY5NzYiCiAgICAgeD0iLTAuODAzNjg5NDgiCiAgICAgeT0iMTIuNTU5MzEyIgogICAgIGlkPSJ0ZXh0ODE4IgogICAgIHRyYW5zZm9ybT0ic2NhbGUoMS4wOTAxOSwwLjkxNzI3MTMpIj48dHNwYW4KICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICBpZD0idHNwYW44MTYiCiAgICAgICB4PSItMC44MDM2ODk0OCIKICAgICAgIHk9IjEyLjU1OTMxMiIKICAgICAgIHN0eWxlPSJmb250LXNpemU6MTQuNjY2NjY2OThweDtmaWxsOiNmZmMxMDc7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlLXdpZHRoOjEuMDIzMTY5NzYiPig8L3RzcGFuPjwvdGV4dD4KICA8dGV4dAogICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgc3R5bGU9ImZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXNpemU6NDAuOTI3MTQzMXB4O2xpbmUtaGVpZ2h0OjEuMjU7Zm9udC1mYW1pbHk6c2Fucy1zZXJpZjtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiNmZmMxMDc7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuMDIzMTc4NDYiCiAgICAgeD0iOS45NjMwNDQyIgogICAgIHk9IjEyLjQ3ODk0NSIKICAgICBpZD0idGV4dDgyOCIKICAgICB0cmFuc2Zvcm09InNjYWxlKDEuMDkwMTk5MywwLjkxNzI2MzQ4KSI+PHRzcGFuCiAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgaWQ9InRzcGFuODI2IgogICAgICAgeD0iOS45NjMwNDQyIgogICAgICAgeT0iMTIuNDc4OTQ1IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZToxNC42NjY2NjY5OHB4O2ZpbGw6I2ZmYzEwNztmaWxsLW9wYWNpdHk6MTtzdHJva2Utd2lkdGg6MS4wMjMxNzg0NiI+KTwvdHNwYW4+PC90ZXh0PgogIDxwYXRoCiAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICBkPSJtIDExLjkyNCw0LjA2NTk5OTIgLTQuOTMyMDAwMSw0Ljk3IC0yLjgyOCwtMi44MyBMIDIuNzUsNy42MTc5OTkyIDYuOTkxOTk5OSwxMS44NjEgMTMuMzU3LDUuNDk1OTk5MiBsIC0xLjQzMywtMS40MzIgeiIKICAgICBpZD0icGF0aDgxNiIKICAgICBzdHlsZT0iZmlsbDojZmZjMTA3O2ZpbGwtb3BhY2l0eToxIiAvPgo8L3N2Zz4K); + } +} + +.text-box { + flex: 1 0; + height: 44px; + align-self: center; + font-size: 1.4em; + padding-top: 14px; + hyphens: auto; +} + +.date-box { + display: flex; + flex-direction: column; + flex: 1 0; + padding: 0 2px; + align-items: center; + justify-content: center; + + .month, .dow { + font-size: 1.2em; + color: var(--color-text-lighter); + } + .day { + font-size: 1.8em; + margin: 5px 0 5px 0; + } +} + +@media (max-width: (480px) ) { + .vote-header { + padding: 4px 0; + display: flex; + flex-direction: row; + justify-content: space-around; + border-top: 1px solid var(--color-border-dark); + + .date-box { + padding: 0 20px 0 4px; + align-content: center; + } + .counter { + flex-direction: column; + align-items: baseline; + & > * { + margin: 4px 1px; + } + } + } +} + +</style> diff --git a/src/js/components/VoteTable/VoteTableItem.vue b/src/js/components/VoteTable/VoteTableItem.vue new file mode 100644 index 00000000..1e3526c0 --- /dev/null +++ b/src/js/components/VoteTable/VoteTableItem.vue @@ -0,0 +1,169 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div class="vote-item" :class="[answer, { active: isActive}]"> + <div class="icon" @click="voteClick()" /> + </div> +</template> + +<script> +import { mapState } from 'vuex' + +export default { + name: 'VoteTableItem', + + props: { + option: { + type: Object, + default: undefined + }, + userId: { + type: String, + default: '' + } + }, + + computed: { + ...mapState({ + event: state => state.event, + acl: state => state.acl + }), + + answer() { + try { + return this.$store.getters.getVote({ + option: this.option, + userId: this.userId + }).voteAnswer + } catch (e) { + return '' + } + }, + + isValidUser() { + return (this.userId !== '' && this.userId !== null) + }, + + isActive() { + return (this.isValidUser && this.acl.userId === this.userId && !this.event.expired) + } + + }, + + methods: { + voteClick() { + if (this.isActive) { + this.$emit('voteClick') + } + } + + } +} + +</script> + +<style lang="scss" scoped> + $bg-no: #ffede9; + $bg-maybe: #fcf7e1; + $bg-unvoted: #fff4c8; + $bg-yes: #ebf5d6; + + $fg-no: #f45573; + $fg-maybe: #f0db98; + $fg-unvoted: #f0db98; + $fg-yes: #49bc49; + + .vote-item { + height: 43px; + display: flex; + flex: 1; + width: 85px; + align-items: center; + background-color: var(--color-background-dark); + color: var(--color-main-text); + > .icon { + margin: auto; + background-position: center; + background-repeat: no-repeat; + background-size: 32px; + background-image: var(--icon-close-000); + min-width: 40px; + min-height: 40px; + width: 40px; + height: 40px; + background-size: 90%; + flex: 0 0 auto; + } + + &.yes { + background-color: $bg-yes; + color: $fg-yes; + // background-image: var(--icon-checkmark-49bc49); + > .icon { + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTYiIHdpZHRoPSIxNiIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxwYXRoIGQ9Im0yLjM1IDcuMyA0IDRsNy4zLTcuMyIgc3Ryb2tlPSIjNDliYzQ5IiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9Im5vbmUiLz48L3N2Zz4K); + } + } + + &.no { + background-color: $bg-no; + color: $fg-no; + // background-image: var(--icon-close-f45573); + > .icon { + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTYiIHdpZHRoPSIxNiIgdmVyc2lvbj0iMS4xIiB2aWV3Ym94PSIwIDAgMTYgMTYiPjxwYXRoIGQ9Im0xNCAxMi4zLTEuNyAxLjctNC4zLTQuMy00LjMgNC4zLTEuNy0xLjcgNC4zLTQuMy00LjMtNC4zIDEuNy0xLjcgNC4zIDQuMyA0LjMtNC4zIDEuNyAxLjctNC4zIDQuM3oiIGZpbGw9IiNmNDU1NzMiLz48L3N2Zz4K); + } + } + + &.maybe { + background-color: $bg-maybe; + color: $fg-maybe; + // background-image: var(--icon-polls-maybe-vote-variant-f0db98); + > .icon { + background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgaWQ9InN2ZzQiCiAgIHZlcnNpb249IjEuMSIKICAgd2lkdGg9IjE2IgogICBoZWlnaHQ9IjE2IgogICBzb2RpcG9kaTpkb2NuYW1lPSJtYXliZS12b3RlLXZhcmlhbnQuc3ZnIgogICBpbmtzY2FwZTp2ZXJzaW9uPSIwLjkyLjIgKDVjM2U4MGQsIDIwMTctMDgtMDYpIj4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjNjY2NjY2IgogICAgIGJvcmRlcm9wYWNpdHk9IjEiCiAgICAgb2JqZWN0dG9sZXJhbmNlPSIxMCIKICAgICBncmlkdG9sZXJhbmNlPSIxMCIKICAgICBndWlkZXRvbGVyYW5jZT0iMTAiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjE5MjAiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iMTAxNyIKICAgICBpZD0ibmFtZWR2aWV3NiIKICAgICBzaG93Z3JpZD0iZmFsc2UiCiAgICAgaW5rc2NhcGU6em9vbT0iMTQuNzUiCiAgICAgaW5rc2NhcGU6Y3g9IjgiCiAgICAgaW5rc2NhcGU6Y3k9IjE0Ljg2NTIwMSIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iLTgiCiAgICAgaW5rc2NhcGU6d2luZG93LXk9Ii04IgogICAgIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjEiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ic3ZnNCI+CiAgICA8aW5rc2NhcGU6Z3JpZAogICAgICAgdHlwZT0ieHlncmlkIgogICAgICAgaWQ9ImdyaWQ4MzYiIC8+CiAgPC9zb2RpcG9kaTpuYW1lZHZpZXc+CiAgPG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhMTAiPgogICAgPHJkZjpSREY+CiAgICAgIDxjYzpXb3JrCiAgICAgICAgIHJkZjphYm91dD0iIj4KICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4KICAgICAgICA8ZGM6dHlwZQogICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+CiAgICAgICAgPGRjOnRpdGxlPjwvZGM6dGl0bGU+CiAgICAgIDwvY2M6V29yaz4KICAgIDwvcmRmOlJERj4KICA8L21ldGFkYXRhPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM4IiAvPgogIDx0ZXh0CiAgICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc2l6ZToxNS4wMDY0OTA3MXB4O2xpbmUtaGVpZ2h0OjEuMjU7Zm9udC1mYW1pbHk6c2Fucy1zZXJpZjtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiNmZmMxMDc7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuMDIzMTY5NzYiCiAgICAgeD0iLTAuODAzNjg5NDgiCiAgICAgeT0iMTIuNTU5MzEyIgogICAgIGlkPSJ0ZXh0ODE4IgogICAgIHRyYW5zZm9ybT0ic2NhbGUoMS4wOTAxOSwwLjkxNzI3MTMpIj48dHNwYW4KICAgICAgIHNvZGlwb2RpOnJvbGU9ImxpbmUiCiAgICAgICBpZD0idHNwYW44MTYiCiAgICAgICB4PSItMC44MDM2ODk0OCIKICAgICAgIHk9IjEyLjU1OTMxMiIKICAgICAgIHN0eWxlPSJmb250LXNpemU6MTQuNjY2NjY2OThweDtmaWxsOiNmZmMxMDc7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlLXdpZHRoOjEuMDIzMTY5NzYiPig8L3RzcGFuPjwvdGV4dD4KICA8dGV4dAogICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgc3R5bGU9ImZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtd2VpZ2h0Om5vcm1hbDtmb250LXNpemU6NDAuOTI3MTQzMXB4O2xpbmUtaGVpZ2h0OjEuMjU7Zm9udC1mYW1pbHk6c2Fucy1zZXJpZjtsZXR0ZXItc3BhY2luZzowcHg7d29yZC1zcGFjaW5nOjBweDtmaWxsOiNmZmMxMDc7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuMDIzMTc4NDYiCiAgICAgeD0iOS45NjMwNDQyIgogICAgIHk9IjEyLjQ3ODk0NSIKICAgICBpZD0idGV4dDgyOCIKICAgICB0cmFuc2Zvcm09InNjYWxlKDEuMDkwMTk5MywwLjkxNzI2MzQ4KSI+PHRzcGFuCiAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgaWQ9InRzcGFuODI2IgogICAgICAgeD0iOS45NjMwNDQyIgogICAgICAgeT0iMTIuNDc4OTQ1IgogICAgICAgc3R5bGU9ImZvbnQtc2l6ZToxNC42NjY2NjY5OHB4O2ZpbGw6I2ZmYzEwNztmaWxsLW9wYWNpdHk6MTtzdHJva2Utd2lkdGg6MS4wMjMxNzg0NiI+KTwvdHNwYW4+PC90ZXh0PgogIDxwYXRoCiAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICBkPSJtIDExLjkyNCw0LjA2NTk5OTIgLTQuOTMyMDAwMSw0Ljk3IC0yLjgyOCwtMi44MyBMIDIuNzUsNy42MTc5OTkyIDYuOTkxOTk5OSwxMS44NjEgMTMuMzU3LDUuNDk1OTk5MiBsIC0xLjQzMywtMS40MzIgeiIKICAgICBpZD0icGF0aDgxNiIKICAgICBzdHlsZT0iZmlsbDojZmZjMTA3O2ZpbGwtb3BhY2l0eToxIiAvPgo8L3N2Zz4K); + } + } + + &.active { + background-color: var(--color-main-background); + > .icon { + cursor: pointer; + border: 2px solid; + border-radius: var(--border-radius); + } + &:active { + box-shadow: inherit; + } + } + + } + + @media (max-width: (480px) ) { + .vote-item { + border-top: 1px solid var(--color-border-dark); + &.active { + width: 10vw; + height: 10vw; + } + } + } + +</style> diff --git a/src/js/main.js b/src/js/main.js new file mode 100644 index 00000000..28877f5b --- /dev/null +++ b/src/js/main.js @@ -0,0 +1,70 @@ +/* jshint esversion: 6 */ +/** + * @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + * + * @author René Gieling <github@dartcafe.de> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import Vue from 'vue' +import axios from '@nextcloud/axios' + +import App from './App' +import store from './store' +import router from './router' +import vClickOutside from 'v-click-outside' +import VueClipboard from 'vue-clipboard2' + +import { PopoverMenu, Tooltip, DatetimePicker, AppContent } from '@nextcloud/vue' + +import UserDiv from './components/Base/UserDiv' +import LoadingOverlay from './components/Base/LoadingOverlay' + +/* eslint-disable-next-line camelcase, no-undef */ +__webpack_nonce__ = btoa(OC.requestToken) +/* eslint-disable-next-line camelcase, no-undef */ +__webpack_public_path__ = OC.linkTo('polls', 'js/') + +Vue.config.debug = process.env.NODE_ENV !== 'production' +Vue.config.devTools = process.env.NODE_ENV !== 'production' +Vue.config.performance = process.env.NODE_ENV !== 'production' + +Vue.prototype.t = t +Vue.prototype.n = n +Vue.prototype.$http = axios +Vue.prototype.OC = OC +Vue.prototype.OCA = OCA + +Vue.component('PopoverMenu', PopoverMenu) +Vue.component('AppContent', AppContent) +Vue.component('DatePicker', DatetimePicker) +Vue.component('UserDiv', UserDiv) +Vue.component('LoadingOverlay', LoadingOverlay) + +Vue.directive('tooltip', Tooltip) + +Vue.use(vClickOutside) +Vue.use(VueClipboard) + +/* eslint-disable-next-line no-new */ +new Vue({ + el: '#app-polls', + router: router, + store: store, + render: h => h(App) +}) diff --git a/src/js/router.js b/src/js/router.js new file mode 100644 index 00000000..1e6ccebc --- /dev/null +++ b/src/js/router.js @@ -0,0 +1,81 @@ +/* jshint esversion: 6 */ +/** + * @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net> + * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author Julius Härtl <jus@bitgrid.net> + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +import Vue from 'vue' +import Router from 'vue-router' + +// Dynamic loading +const List = () => import('./views/PollList') +const Vote = () => import('./views/Vote') +const PublicVote = () => import('./views/PublicVote') + +Vue.use(Router) + +export default new Router({ + mode: 'history', + base: OC.generateUrl(''), + linkActiveClass: 'active', + routes: [ + { + path: '/:index(index.php/)?apps/polls/:type?', + components: { + default: List + }, + props: true, + name: 'list' + }, + { + path: '/:index(index.php/)?apps/polls/vote/:id', + components: { + default: Vote + }, + props: true, + name: 'vote' + }, + { + path: '/:index(index.php/)?apps/polls/vote/:id', + components: { + default: Vote + }, + props: true, + name: 'clone' + }, + { + path: '/:index(index.php/)?apps/polls/vote/:id', + components: { + default: Vote + }, + props: true, + name: 'create' + }, + { + path: '/:index(index.php/)?apps/polls/s/:token', + components: { + default: PublicVote + }, + props: true, + name: 'publicVote' + } + ] +}) diff --git a/src/js/store/index.js b/src/js/store/index.js new file mode 100644 index 00000000..cdf31566 --- /dev/null +++ b/src/js/store/index.js @@ -0,0 +1,55 @@ +/* + * @copyright Copyright (c) 2019 Rene Gieling <github@dartcafe.de> + * + * @author Rene Gieling <github@dartcafe.de> + * @author John Molakvoæ <skjnldsv@protonmail.com> + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import Vue from 'vue' +import Vuex from 'vuex' +import acl from './modules/acl' +import comments from './modules/comments' +import event from './modules/event' +import notification from './modules/notification' +import votes from './modules/votes' +import options from './modules/options' +import shares from './modules/shares' +import locale from './modules/locale' + +Vue.use(Vuex) + +/* eslint-disable-next-line no-unused-vars */ +const debug = process.env.NODE_ENV !== 'production' + +export default new Vuex.Store({ + + modules: { + acl, + comments, + event, + notification, + locale, + votes, + options, + shares + }, + + strict: process.env.NODE_ENV !== 'production' +}) diff --git a/src/js/store/modules/acl.js b/src/js/store/modules/acl.js new file mode 100644 index 00000000..85e1fec5 --- /dev/null +++ b/src/js/store/modules/acl.js @@ -0,0 +1,82 @@ +/* + * @copyright Copyright (c) 2019 Rene Gieling <github@dartcafe.de> + * + * @author Rene Gieling <github@dartcafe.de> + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import axios from '@nextcloud/axios' + +const defaultAcl = () => { + return { + userId: null, + pollId: null, + token: null, + isOwner: false, + isAdmin: false, + allowView: false, + allowVote: false, + allowComment: false, + allowEdit: false, + allowSeeUsernames: false, + allowSeeAllVotes: false, + foundByToken: false, + accessLevel: '' + } +} + +const state = defaultAcl() + +const mutations = { + + setAcl(state, payload) { + Object.assign(state, payload.acl) + }, + + reset(state) { + Object.assign(state, defaultAcl()) + } + +} + +const actions = { + + loadPoll({ commit, rootState }, payload) { + commit('reset') + let endPoint = 'apps/polls/get/acl/' + + if (payload.token !== undefined) { + endPoint = endPoint.concat('s/', payload.token) + } else if (payload.pollId !== undefined) { + endPoint = endPoint.concat(payload.pollId) + } else { + return + } + + return axios.get(OC.generateUrl(endPoint)) + .then((response) => { + commit('setAcl', { 'acl': response.data }) + }, (error) => { + console.error('Error loading comments', { 'error': error.response }, { 'payload': payload }) + throw error + }) + } +} + +export default { state, mutations, actions } diff --git a/src/js/store/modules/comments.js b/src/js/store/modules/comments.js new file mode 100644 index 00000000..a73620c9 --- /dev/null +++ b/src/js/store/modules/comments.js @@ -0,0 +1,95 @@ +/* + * @copyright Copyright (c) 2019 Rene Gieling <github@dartcafe.de> + * + * @author Rene Gieling <github@dartcafe.de> + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import axios from '@nextcloud/axios' +import sortBy from 'lodash/sortBy' + +const defaultComments = () => { + return { + list: [] + } +} + +const state = defaultComments() + +const mutations = { + + setComments(state, payload) { + Object.assign(state, payload) + }, + + reset(state) { + Object.assign(state, defaultComments()) + }, + + addComment(state, payload) { + state.list.push(payload) + } + +} + +const getters = { + sortedComments: state => { + return sortBy(state.list, 'date').reverse() + }, + + countComments: state => { + return state.list.length + } +} + +const actions = { + + loadPoll({ commit, rootState }, payload) { + commit('reset') + let endPoint = 'apps/polls/get/comments/' + + if (payload.token !== undefined) { + endPoint = endPoint.concat('s/', payload.token) + } else if (payload.pollId !== undefined) { + endPoint = endPoint.concat(payload.pollId) + } else { + return + } + + return axios.get(OC.generateUrl(endPoint)) + .then((response) => { + commit('setComments', { 'list': response.data }) + }, (error) => { + console.error('Error loading comments', { 'error': error.response }, { 'payload': payload }) + throw error + }) + }, + + writeCommentPromise({ commit, rootState }, payload) { + return axios.post(OC.generateUrl('apps/polls/write/comment'), { pollId: rootState.event.id, message: payload }) + .then((response) => { + commit('addComment', response.data) + }, (error) => { + console.error('Error writing comment', { 'error': error.response }, { 'payload': payload }) + throw error + }) + } +} + +export default { state, mutations, actions, getters } diff --git a/src/js/store/modules/event.js b/src/js/store/modules/event.js new file mode 100644 index 00000000..ba34b5bf --- /dev/null +++ b/src/js/store/modules/event.js @@ -0,0 +1,155 @@ +/* + * @copyright Copyright (c) 2019 Rene Gieling <github@dartcafe.de> + * + * @author Rene Gieling <github@dartcafe.de> + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import axios from '@nextcloud/axios' +import moment from 'moment' + +const defaultEvent = () => { + return { + id: 0, + type: 'datePoll', + title: '', + description: '', + owner: undefined, + created: '', + access: 'public', + expire: null, + isAnonymous: false, + fullAnonymous: false, + allowMaybe: false, + voteLimit: null, + showResults: true, + deleted: false, + deleteDate: null + } +} + +const state = defaultEvent() + +const mutations = { + setEvent(state, payload) { + Object.assign(state, payload.event) + }, + + resetEvent(state) { + Object.assign(state, defaultEvent()) + }, + + setEventProperty(state, payload) { + Object.assign(state, payload) + } + +} + +const getters = { + + timeSpanCreated: state => { + return moment(state.created).fromNow() + }, + + isExpirationSet: state => { + return Boolean(moment(state.expire).unix()) + }, + + expired: (state, getters) => { + return (getters.isExpirationSet && moment(state.expire).diff() < 0) + }, + + timeSpanExpiration: (state, getters) => { + if (getters.expired) { + return moment(state.expire).fromNow() + } else { + return t('polls', 'never') + } + }, + + accessType: (state, getters, rootState) => { + if (rootState.acl.accessLevel === 'public') { + return t('polls', 'Public access') + } else if (rootState.acl.accessLevel === 'select') { + return t('polls', 'Only shared') + } else if (rootState.acl.accessLevel === 'registered') { + return t('polls', 'Registered users only') + } else if (rootState.acl.accessLevel === 'hidden') { + return t('polls', 'Hidden poll') + } else { + return rootState.acl.accessLevel + } + }, + + allowEdit: (state, getters, rootState) => { + return (rootState.acl.allowEdit) + } + +} + +const actions = { + + loadEvent({ commit }, payload) { + commit('resetEvent') + let endPoint = 'apps/polls/get/event/' + + if (payload.token !== undefined) { + endPoint = endPoint.concat('s/', payload.token) + } else if (payload.pollId !== undefined) { + endPoint = endPoint.concat(payload.pollId) + } else { + return + } + + return axios.get(OC.generateUrl(endPoint)) + .then((response) => { + commit('setEvent', { 'event': response.data }) + }, (error) => { + if (error.response.status !== '404') { + console.error('Error loading event', { 'error': error.response }, { 'payload': payload }) + } + throw error + }) + }, + + deleteEventPromise({ commit }, payload) { + return axios.post(OC.generateUrl('apps/polls/delete/event'), { event: payload.id }) + .then((response) => { + return response + }, (error) => { + console.error('Error deleting event', { 'error': error.response }, { 'payload': payload }) + throw error + }) + + }, + + writeEventPromise({ commit, rootState }) { + return axios.post(OC.generateUrl('apps/polls/write/event'), { event: state }) + .then((response) => { + commit('setEvent', { 'event': response.data }) + return response.event + }, (error) => { + console.error('Error writing event:', { 'error': error.response }, { 'state': state }) + throw error + }) + + } +} + +export default { state, mutations, getters, actions, defaultEvent } diff --git a/src/js/store/modules/locale.js b/src/js/store/modules/locale.js new file mode 100644 index 00000000..58f92146 --- /dev/null +++ b/src/js/store/modules/locale.js @@ -0,0 +1,60 @@ +/* + * @copyright Copyright (c) 2019 Rene Gieling <github@dartcafe.de> + * + * @author Rene Gieling <github@dartcafe.de> + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import moment from 'moment' + +const getters = { + longDateFormat() { + return moment.localeData().longDateFormat('L') + }, + + dateTimeFormat() { + return moment.localeData().longDateFormat('L') + ' ' + moment.localeData().longDateFormat('LT') + }, + + languageCode() { + return OC.getLanguage() + }, + + languageCodeShort() { + return OC.getLanguage().split('-')[0] + }, + + localeCode() { + try { + return OC.getLocale() + } catch (e) { + if (e instanceof TypeError) { + return OC.getLanguage() + } else { + console.error(e) + } + } + }, + + localeData(getters) { + return moment.localeData(moment.locale(getters.localeCode)) + } +} + +export default { getters } diff --git a/src/js/store/modules/notification.js b/src/js/store/modules/notification.js new file mode 100644 index 00000000..9049ca4b --- /dev/null +++ b/src/js/store/modules/notification.js @@ -0,0 +1,62 @@ +/* + * @copyright Copyright (c) 2019 Rene Gieling <github@dartcafe.de> + * + * @author Rene Gieling <github@dartcafe.de> + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import axios from '@nextcloud/axios' + +const defaultNotification = () => { + return { + subscribed: false + } +} + +const state = defaultNotification() + +const mutations = { + + setNotification(state, payload) { + state.subscribed = payload + } + +} + +const actions = { + getSubscription({ commit }, payload) { + axios.get(OC.generateUrl('apps/polls/get/notification/' + payload.pollId)) + .then((response) => { + commit('setNotification', true) + }) + .catch(() => { + commit('setNotification', false) + }) + }, + + writeSubscriptionPromise({ commit }, payload) { + return axios.post(OC.generateUrl('apps/polls/set/notification'), { pollId: payload.pollId, subscribed: state.subscribed }) + .then((response) => { + }, (error) => { + console.error(error.response) + }) + } +} + +export default { state, mutations, actions } diff --git a/src/js/store/modules/options.js b/src/js/store/modules/options.js new file mode 100644 index 00000000..6e8d0496 --- /dev/null +++ b/src/js/store/modules/options.js @@ -0,0 +1,141 @@ +/* + * @copyright Copyright (c) 2019 Rene Gieling <github@dartcafe.de> + * + * @author Rene Gieling <github@dartcafe.de> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import axios from '@nextcloud/axios' +import sortBy from 'lodash/sortBy' +import moment from 'moment' + +const defaultOptions = () => { + return { + list: [] + } +} + +const state = defaultOptions() + +const mutations = { + optionsSet(state, payload) { + Object.assign(state, payload) + }, + + reset(state) { + Object.assign(state, defaultOptions()) + }, + + optionRemove(state, payload) { + state.list = state.list.filter(option => { + return option.id !== payload.option.id + }) + }, + + setOption(state, payload) { + let index = state.list.findIndex((option) => { + return option.id === payload.option.id + }) + + if (index < 0) { + state.list.push(payload.option) + } else { + state.list.splice(index, 1, payload.option) + } + } +} + +const getters = { + lastOptionId: state => { + return Math.max.apply(Math, state.list.map(function(option) { return option.id })) + }, + + sortedOptions: state => { + return sortBy(state.list, 'timestamp') + } +} + +const actions = { + + loadPoll({ commit, rootState }, payload) { + commit('reset') + let endPoint = 'apps/polls/get/options/' + + if (payload.token !== undefined) { + endPoint = endPoint.concat('s/', payload.token) + } else if (payload.pollId !== undefined) { + endPoint = endPoint.concat(payload.pollId) + } else { + return + } + + return axios.get(OC.generateUrl(endPoint)) + .then((response) => { + commit('optionsSet', { 'list': response.data }) + }, (error) => { + console.error('Error loading options', { 'error': error.response }, { 'payload': payload }) + throw error + }) + }, + + updateOptionAsync({ commit, getters, dispatch, rootState }, payload) { + return axios.post(OC.generateUrl('apps/polls/update/option'), { option: payload.option }) + .then((response) => { + commit('setOption', { 'option': payload.option }) + }, (error) => { + console.error('Error updating option', { 'error': error.response }, { 'payload': payload }) + throw error + }) + }, + + addOptionAsync({ commit, getters, dispatch, rootState }, payload) { + let option = {} + + option.id = 0 + option.pollId = rootState.event.id + + if (rootState.event.type === 'datePoll') { + option.timestamp = moment(payload.pollOptionText).unix() + option.pollOptionText = moment.utc(payload.pollOptionText).format('YYYY-MM-DD HH:mm:ss') + + } else if (rootState.event.type === 'textPoll') { + option.timestamp = 0 + option.pollOptionText = payload.pollOptionText + } + + return axios.post(OC.generateUrl('apps/polls/add/option'), { option: option }) + .then((response) => { + commit('setOption', { 'option': response.data }) + }, (error) => { + console.error('Error adding option', { 'error': error.response }, { 'payload': payload }) + throw error + }) + }, + + removeOptionAsync({ commit, getters, dispatch, rootState }, payload) { + return axios.post(OC.generateUrl('apps/polls/remove/option'), { option: payload.option }) + .then((response) => { + commit('optionRemove', { 'option': payload.option }) + }, (error) => { + console.error('Error removing option', { 'error': error.response }, { 'payload': payload }) + throw error + }) + } +} + +export default { state, mutations, getters, actions } diff --git a/src/js/store/modules/shares.js b/src/js/store/modules/shares.js new file mode 100644 index 00000000..0848fa20 --- /dev/null +++ b/src/js/store/modules/shares.js @@ -0,0 +1,153 @@ +/* + * @copyright Copyright (c) 2019 Rene Gieling <github@dartcafe.de> + * + * @author Rene Gieling <github@dartcafe.de> + * @author Julius Härtl <jus@bitgrid.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import axios from '@nextcloud/axios' + +const defaultShares = () => { + return { + list: [] + } +} + +const state = defaultShares() + +const mutations = { + setShares(state, payload) { + Object.assign(state, payload) + }, + + removeShare(state, payload) { + state.list = state.list.filter(share => { + return share.id !== payload.share.id + }) + }, + + reset(state) { + Object.assign(state, defaultShares()) + }, + + addShare(state, payload) { + state.list.push(payload) + } + +} + +const getters = { + sortedShares: state => { + return state.list + }, + + invitationShares: state => { + let invitationTypes = ['user', 'group', 'mail', 'external'] + return state.list.filter(function(share) { + return invitationTypes.includes(share.type) + }) + }, + + publicShares: state => { + let invitationTypes = ['public'] + return state.list.filter(function(share) { + return invitationTypes.includes(share.type) + }) + }, + + countShares: state => { + return state.list.length + } +} + +const actions = { + loadPoll({ commit, rootState }, payload) { + commit('reset') + // console.log(rootState.acl) + // if (!rootState.acl.allowEdit) { + // console.log('rootState.acl.allowEdit', rootState.acl.allowEdit) + // return + // } + // console.log('number 1') + let endPoint = 'apps/polls/get/shares/' + + if (payload.token !== undefined) { + return + } else if (payload.pollId !== undefined) { + endPoint = endPoint.concat(payload.pollId) + } else { + return + } + + return axios.get(OC.generateUrl(endPoint)) + .then((response) => { + commit('setShares', { + 'list': response.data + }) + }, (error) => { + console.error('Error loading shares', { 'error': error.response }, { 'payload': payload }) + throw error + }) + }, + + getShareAsync({ commit }, payload) { + return axios.get(OC.generateUrl('apps/polls/get/share/' + payload.token)) + .then((response) => { + return { 'share': response.data } + }, (error) => { + console.error('Error loading share', { 'error': error.response }, { 'payload': payload }) + throw error + }) + }, + + addShareFromUser({ commit }, payload) { + return axios.post(OC.generateUrl('apps/polls/write/share/s'), { token: payload.token, userName: payload.userName }) + .then((response) => { + return { 'token': response.data.token } + }, (error) => { + console.error('Error writing share', { 'error': error.response }, { 'payload': payload }) + throw error + }) + + }, + + writeSharePromise({ commit, rootState }, payload) { + payload.share.pollId = rootState.event.id + return axios.post(OC.generateUrl('apps/polls/write/share'), { pollId: rootState.event.id, share: payload.share }) + .then((response) => { + commit('addShare', response.data) + }, (error) => { + console.error('Error writing share', { 'error': error.response }, { 'payload': payload }) + throw error + }) + }, + + removeShareAsync({ commit, getters, dispatch, rootState }, payload) { + return axios.post(OC.generateUrl('apps/polls/remove/share'), { share: payload.share }) + .then((response) => { + commit('removeShare', { 'share': payload.share }) + }, (error) => { + console.error('Error removing share', { 'error': error.response }, { 'payload': payload }) + throw error + }) + } + +} + +export default { state, mutations, actions, getters } diff --git a/src/js/store/modules/votes.js b/src/js/store/modules/votes.js new file mode 100644 index 00000000..82ef335d --- /dev/null +++ b/src/js/store/modules/votes.js @@ -0,0 +1,168 @@ +/* + * @copyright Copyright (c) 2019 Rene Gieling <github@dartcafe.de> + * + * @author Rene Gieling <github@dartcafe.de> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import axios from '@nextcloud/axios' +import orderBy from 'lodash/orderBy' + +const defaultVotes = () => { + return { + list: [] + } +} + +const state = defaultVotes() + +const mutations = { + reset(state) { + Object.assign(state, defaultVotes()) + }, + + setVotes(state, payload) { + Object.assign(state, payload) + }, + + setVote(state, payload) { + let index = state.list.findIndex(vote => + parseInt(vote.pollId) === payload.pollId + && vote.userId === payload.vote.userId + && vote.voteOptionText === payload.option.pollOptionText) + if (index > -1) { + state.list[index] = Object.assign(state.list[index], payload.vote) + } else { + state.list.push(payload.vote) + } + } +} + +const getters = { + + answerSequence: (state, getters, rootState) => { + if (rootState.event.allowMaybe) { + return ['no', 'maybe', 'yes', 'no'] + } else { + return ['no', 'yes', 'no'] + } + }, + + participants: (state, getters, rootState) => { + let list = [] + state.list.forEach(function(vote) { + if (!list.includes(vote.userId)) { + list.push(vote.userId) + } + }) + + if (!list.includes(rootState.acl.userId) && rootState.acl.userId !== null) { + list.push(rootState.acl.userId) + } + + return list + }, + + votesRank: (state, getters, rootGetters) => { + let rank = [] + rootGetters.options.list.forEach(function(option) { + let countYes = state.list.filter(vote => vote.voteOptionText === option.pollOptionText && vote.voteAnswer === 'yes').length + let countMaybe = state.list.filter(vote => vote.voteOptionText === option.pollOptionText && vote.voteAnswer === 'maybe').length + let countNo = state.list.filter(vote => vote.voteOptionText === option.pollOptionText && vote.voteAnswer === 'no').length + rank.push({ + 'rank': 0, + 'pollOptionText': option.pollOptionText, + 'yes': countYes, + 'no': countNo, + 'maybe': countMaybe + }) + }) + return orderBy(rank, ['yes', 'maybe'], ['desc', 'desc']) + }, + + winnerCombo: (state, getters) => { + return getters.votesRank[0] + }, + + getVote: (state, getters) => (payload) => { + return state.list.find(vote => { + return (vote.userId === payload.userId + && vote.voteOptionText === payload.option.pollOptionText) + }) + }, + + getNextAnswer: (state, getters) => (payload) => { + try { + return getters.answerSequence[getters.answerSequence.indexOf(getters.getVote(payload).voteAnswer) + 1] + } catch (e) { + return getters.answerSequence[1] + } + + } + +} + +const actions = { + + loadPoll({ commit, rootState }, payload) { + commit('reset') + let endPoint = 'apps/polls/get/votes/' + if (payload.token !== undefined) { + endPoint = endPoint.concat('s/', payload.token) + } else if (payload.pollId !== undefined) { + endPoint = endPoint.concat(payload.pollId) + } else { + return + } + + axios.get(OC.generateUrl(endPoint)) + .then((response) => { + commit('setVotes', { 'list': response.data }) + }, (error) => { + console.error('Error loading votes', { 'error': error.response }, { 'payload': payload }) + throw error + }) + }, + + setVoteAsync({ commit, getters, rootState }, payload) { + + let endPoint = 'apps/polls/set/vote/' + + if (rootState.acl.foundByToken) { + endPoint = endPoint.concat('s/') + } + + return axios.post(OC.generateUrl(endPoint), { + pollId: rootState.event.id, + token: rootState.acl.token, + option: payload.option, + userId: payload.userId, + setTo: payload.setTo + }) + .then((response) => { + commit('setVote', { option: payload.option, pollId: rootState.event.id, vote: response.data }) + return response.data + }, (error) => { + console.error('Error setting vote', { 'error': error.response }, { 'payload': payload }) + throw error + }) + } + +} + +export default { state, mutations, getters, actions } diff --git a/src/js/views/PollList.vue b/src/js/views/PollList.vue new file mode 100644 index 00000000..fddb5326 --- /dev/null +++ b/src/js/views/PollList.vue @@ -0,0 +1,181 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <app-content> + <div class="main-container"> + <div v-if="noPolls" class=""> + <div class="icon-polls" /> + <h2> {{ t('No existing polls.') }} </h2> + <router-link :to="{ name: 'create'}" class="button new"> + <span>{{ t('polls', 'Click here to add a poll') }}</span> + </router-link> + </div> + + <transition-group v-if="!noPolls" name="list" tag="div" + class="table"> + <PollListItem key="0" :header="true" /> + <li is="PollListItem" + v-for="(poll, index) in pollList" + :key="poll.id" + :poll="poll" + @deletePoll="removePoll(index, poll)" + @editPoll="callPoll(index, poll, 'edit')" + @clonePoll="callPoll(index, poll, 'clone')" /> + </transition-group> + </div> + <loading-overlay v-if="loading" /> + <!-- <modal-dialog /> --> + </app-content> +</template> + +<script> +import PollListItem from '../components/PollList/PollListItem' +import { mapGetters } from 'vuex' +export default { + name: 'PollList', + + components: { + PollListItem + }, + + data() { + return { + noPolls: false, + loading: true, + pollList: this.$store.state.polls.list + } + }, + + computed: { + ...mapGetters([ + 'myPolls', + 'publicPolls', + 'hiddenPolls', + 'deletedPolls' + ]) + + // pollList() { + // return this.$store.state.polls.list + // } + }, + + watch: { + $route(to, from) { + if (this.$route.params.type === 'all') { + this.pollList = this.$store.state.polls.list + } else if (this.$route.params.type === 'my') { + this.pollList = this.myPolls + } else if (this.$route.params.type === 'public') { + this.pollList = this.publicPolls + } else if (this.$route.params.type === 'hidden') { + this.pollList = this.hiddenPolls + } else if (this.$route.params.type === 'deleted') { + this.pollList = this.deletedPolls + } + } + }, + + created() { + this.refreshPolls() + }, + + methods: { + callPoll(index, event, name) { + this.$router.push({ + name: name, + params: { + id: event.id + } + }) + }, + + refreshPolls() { + this.loading = true + this.$store + .dispatch('loadPolls') + .then(response => { + this.loading = false + }) + .catch(error => { + this.loading = false + console.error('refresh poll: ', error.response) + OC.Notification.showTemporary(t('polls', 'Error loading polls"', 1, event.title), { type: 'error' }) + }) + } + + // removePoll(index, event) { + // const params = { + // title: t('polls', 'Delete poll'), + // text: t('polls', 'Do you want to delete "%n"?', 1, event.title), + // buttonHideText: t('polls', 'No, keep poll.'), + // buttonConfirmText: t('polls', 'Yes, delete poll.'), + // // Call store action here + // onConfirm: () => { + // this.loading = true + // this.$store + // .dispatch({ + // type: 'deletePollPromise', + // event: event + // }) + // .then(response => { + // this.loading = false + // this.refreshPolls() + // OC.Notification.showTemporary(t('polls', 'Poll "%n" deleted', 1, event.title), { type: 'success' }) + // }) + // .catch(error => { + // this.loading = false + // console.error('remove poll: ', error.response) + // OC.Notification.showTemporary(t('polls', 'Error while deleting Poll "%n"', 1, event.title), { type: 'error' }) + // }) + // } + // } + // + // } + } +} +</script> + +<style lang="scss" scoped> + #app-content { + // flex-direction: column; + } + .main-container { + flex: 1; + } + .table { + width: 100%; + // margin-top: 45px; + display: flex; + flex-direction: column; + flex: 1; + flex-wrap: nowrap; + } + + #emptycontent { + .icon-polls { + background-color: black; + -webkit-mask: url('./img/app.svg') no-repeat 50% 50%; + mask: url('./img/app.svg') no-repeat 50% 50%; + } + } +</style> diff --git a/src/js/views/PublicVote.vue b/src/js/views/PublicVote.vue new file mode 100644 index 00000000..4d13f433 --- /dev/null +++ b/src/js/views/PublicVote.vue @@ -0,0 +1,134 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <AppContent> + <div v-if="event.id > 0" class="main-container"> + <a v-if="!sideBarOpen" href="#" class="icon icon-settings active" + :title="t('polls', 'Open Sidebar')" @click="toggleSideBar()" /> + + <VoteHeader /> + <VoteHeaderPublic /> + <VoteTable /> + <Notification /> + </div> + + <SideBar v-if="sideBarOpen" @closeSideBar="toggleSideBar" /> + <LoadingOverlay v-if="loading" /> + </AppContent> +</template> + +<script> +import VoteHeader from '../components/VoteTable/VoteHeader' +import VoteHeaderPublic from '../components/VoteTable/VoteHeaderPublic' +import VoteTable from '../components/VoteTable/VoteTable' +import Notification from '../components/Notification/Notification' +import SideBar from '../components/SideBar/SideBar' +import { mapState } from 'vuex' + +export default { + name: 'Vote', + components: { + Notification, + VoteHeader, + VoteHeaderPublic, + VoteTable, + SideBar + }, + + data() { + return { + voteSaved: false, + delay: 50, + sideBarOpen: false, + initialTab: 'comments' + } + }, + + computed: { + ...mapState({ + event: state => state.event, + acl: state => state.acl + }), + + windowTitle: function() { + return t('polls', 'Polls') + ' - ' + this.event.title + } + + }, + + watch: { + '$route'(to, from) { + this.loadPoll() + } + }, + + mounted() { + this.loadPoll() + }, + + methods: { + loadPoll() { + this.loading = false + this.$store.dispatch('loadEvent', { token: this.$route.params.token }) + .then((response) => { + this.$store.dispatch('loadPoll', { token: this.$route.params.token }) + .then(() => { + this.loading = false + }) + }) + .catch((error) => { + console.error(error) + this.loading = false + }) + }, + + toggleSideBar() { + this.sideBarOpen = !this.sideBarOpen + } + + } +} +</script> + +<style lang="scss" scoped> + .main-container { + flex: 1; + margin: 0; + flex-direction: column; + flex: 1; + flex-wrap: nowrap; + overflow-x: scroll; + h1, h2, h3, h4 { + margin-left: 24px; + } + } + + .icon.icon-settings.active { + display: block; + width: 44px; + height: 44px; + right: 0; + position: absolute; + } + +</style> diff --git a/src/js/views/Vote.vue b/src/js/views/Vote.vue new file mode 100644 index 00000000..e7760f09 --- /dev/null +++ b/src/js/views/Vote.vue @@ -0,0 +1,164 @@ +<!-- + - @copyright Copyright (c) 2018 René Gieling <github@dartcafe.de> + - + - @author René Gieling <github@dartcafe.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <AppContent> + <div v-if="event.id > 0" class="main-container"> + <a v-if="!sideBarOpen" href="#" class="icon icon-settings active" + :title="t('polls', 'Open Sidebar')" @click="toggleSideBar()" /> + <VoteHeader /> + <VoteTable /> + <Notification /> + </div> + + <SideBar v-if="sideBarOpen" @closeSideBar="toggleSideBar" /> + <LoadingOverlay v-if="loading" /> + </AppContent> +</template> + +<script> +import Notification from '../components/Notification/Notification' +import VoteHeader from '../components/VoteTable/VoteHeader' +import VoteTable from '../components/VoteTable/VoteTable' +import SideBar from '../components/SideBar/SideBar' +import { mapState, mapGetters } from 'vuex' + +export default { + name: 'Vote', + components: { + Notification, + VoteHeader, + VoteTable, + SideBar + }, + + data() { + return { + voteSaved: false, + delay: 50, + sideBarOpen: false, + loading: false, + initialTab: 'comments', + newName: '' + } + }, + + computed: { + ...mapState({ + event: state => state.event, + shares: state => state.shares, + acl: state => state.acl + }), + + ...mapGetters([ + 'isExpirationSet', + 'expired', + 'timeSpanExpiration' + ]), + + windowTitle: function() { + return t('polls', 'Polls') + ' - ' + this.event.title + }, + + votePossible() { + return this.acl.allowVote && !this.expired + } + + }, + + watch: { + '$route'(to, from) { + this.loadPoll() + } + }, + + mounted() { + this.loadPoll() + }, + + methods: { + loadPoll() { + this.loading = true + this.$store.dispatch({ type: 'loadEvent', pollId: this.$route.params.id }) + .then((response) => { + this.$store.dispatch({ + type: 'loadPoll', + pollId: this.$route.params.id, + mode: this.$route.name + }) + .then(() => { + if (this.$route.name === 'edit') { + this.openInEditMode() + } + this.loading = false + }) + }) + .catch(() => { + this.loading = false + }) + }, + + toggleSideBar() { + this.sideBarOpen = !this.sideBarOpen + }, + + openConfigurationTab() { + this.initialTab = 'configuration' + this.sideBarOpen = true + this.$store.commit('pollSetProperty', { 'mode': 'edit' }) + }, + + openOptionsTab() { + if (this.event.type === 'datePoll') { + this.initialTab = 'date-options' + } else if (this.event.type === 'textPoll') { + this.initialTab = 'text-options' + } + this.sideBarOpen = true + this.$store.commit('pollSetProperty', { 'mode': 'edit' }) + } + } +} +</script> + +<style lang="scss" scoped> + .main-container { + flex: 1; + margin: 0; + flex-direction: column; + flex: 1; + flex-wrap: nowrap; + overflow-x: scroll; + h1, h2, h3, h4 { + margin-left: 24px; + } + } + + .icon.icon-settings.active { + display: block; + width: 44px; + height: 44px; + right: 0; + position: absolute; + } + +</style> diff --git a/src/js/views/img/app.svg b/src/js/views/img/app.svg new file mode 100644 index 00000000..cbf61396 --- /dev/null +++ b/src/js/views/img/app.svg @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + id="svg8" + viewBox="0 0 32 32" + x="0px" + y="0px" + enable-background="new 0 0 595.275 311.111" + width="32" + height="32" + xml:space="preserve" + version="1.1"><metadata + id="metadata14"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs + id="defs12" /><rect + id="rect2" + y="2" + x="3" + height="26" + width="7" + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.93541431" /><rect + id="rect4" + y="12" + x="12" + height="16" + width="7" + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.93541431" /><rect + id="rect6" + y="8" + x="21" + height="20" + width="7" + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.8918826" /></svg>
\ No newline at end of file diff --git a/src/js/views/img/expired-unvoted-vote.svg b/src/js/views/img/expired-unvoted-vote.svg new file mode 100644 index 00000000..ca46c722 --- /dev/null +++ b/src/js/views/img/expired-unvoted-vote.svg @@ -0,0 +1,14 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"> + <ellipse + style="opacity:1;fill:none;fill-opacity:1;stroke:#f45573;stroke-width:1.49987304;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + cx="8" + cy="8" + rx="6.2500634" + ry="6.2500639" /> + <rect + style="fill:none;fill-opacity:1;stroke:#ffc107;stroke-width:0.86666656;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + width="5.6333332" + height="5.6333332" + x="5.1833334" + y="5.1833334" /> +</svg> diff --git a/src/js/views/img/expired-voted-vote.svg b/src/js/views/img/expired-voted-vote.svg new file mode 100644 index 00000000..a58f41b0 --- /dev/null +++ b/src/js/views/img/expired-voted-vote.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"> + <ellipse + style="opacity:1;fill:none;fill-opacity:1;stroke:#f45573;stroke-width:1.49987304;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + cx="8" + cy="8" + rx="6.2500634" + ry="6.2500639" /> + <path + d="M 6.9766048,11.334813 3.39273,7.7509392 4.4157633,6.7271819 6.9766048,9.285851 11.569757,4.6651869 12.60727,5.7034244 Z" + style="fill:#49bc49;fill-opacity:1" /> +</svg> diff --git a/src/js/views/img/open-unvoted-vote.svg b/src/js/views/img/open-unvoted-vote.svg new file mode 100644 index 00000000..4376c065 --- /dev/null +++ b/src/js/views/img/open-unvoted-vote.svg @@ -0,0 +1,14 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"> + <path + style="opacity:1;fill:none;fill-opacity:1;stroke:#49bc49;stroke-width:1.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="m 14.244065,8.0059972 c 0,3.4518138 -2.798249,6.2500608 -6.2500623,6.2500608 -3.4518138,0 -6.250062,-2.798247 -6.2500622,-6.2500608 0,-3.4518134 2.7982482,-6.2500612 6.2500622,-6.2500612" /> + <path + style="fill:#49bc49;fill-opacity:1;stroke:none;stroke-width:0.11827402;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 7.9774453,0.41685427 11.667719,1.8149286 7.9774453,3.2780296 v 0 z" /> + <rect + style="fill:none;fill-opacity:1;stroke:#ffc107;stroke-width:0.86666656;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + width="5.6333332" + height="5.6333332" + x="5.1833334" + y="5.1833334" /> +</svg> diff --git a/src/js/views/img/open-voted-vote.svg b/src/js/views/img/open-voted-vote.svg new file mode 100644 index 00000000..58e5764f --- /dev/null +++ b/src/js/views/img/open-voted-vote.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"> + <path + d="M 6.9766048,11.334813 3.39273,7.750939 4.4157633,6.7271817 6.9766048,9.2858508 11.569757,4.6651867 12.60727,5.7034243 Z" + style="fill:#49bc49;fill-opacity:1" /> + <path + style="opacity:1;fill:none;fill-opacity:1;stroke:#49bc49;stroke-width:1.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="m 14.250063,7.9999999 c 0,3.4518141 -2.798249,6.2500611 -6.2500635,6.2500611 -3.4518138,0 -6.250062,-2.798247 -6.2500622,-6.2500611 0,-3.4518134 2.7982482,-6.2500609 6.2500622,-6.2500609" /> + <path + style="fill:#49bc49;fill-opacity:1;stroke:none;stroke-width:0.11827402;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 7.977,0.4168247 11.667274,1.8148987 7.977,3.278 v 0 z" /> +</svg> |