Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/polls.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordartcafe <github@dartcafe.de>2019-12-10 09:32:58 +0300
committerdartcafe <github@dartcafe.de>2019-12-10 09:32:58 +0300
commit41930996829421bbf5e26aeb25c93d9214d35e9c (patch)
tree217e7fe3ed394256a324ed7bcbf81c89f0a0c19e /src
parent7363c382904c6ea909010a4939e4ecd40dfdb5eb (diff)
- moved src back in right place
Diffstat (limited to 'src')
-rw-r--r--src/js/App.vue137
-rw-r--r--src/js/assets/app.pngbin0 -> 195 bytes
-rw-r--r--src/js/assets/app.svg38
-rw-r--r--src/js/components/Base/LoadingOverlay.vue59
-rw-r--r--src/js/components/Base/UserDiv.vue148
-rw-r--r--src/js/components/Create/CreateDateItem.vue71
-rw-r--r--src/js/components/Create/CreateDlg.vue125
-rw-r--r--src/js/components/Create/TextPollItem.vue44
-rw-r--r--src/js/components/Navigation/Navigation.vue191
-rw-r--r--src/js/components/Navigation/store/polls.js75
-rw-r--r--src/js/components/Notification/Notification.vue64
-rw-r--r--src/js/components/PollList/PollListItem.vue490
-rw-r--r--src/js/components/Share/ShareDiv.vue172
-rw-r--r--src/js/components/SideBar/CommentAdd.vue99
-rw-r--r--src/js/components/SideBar/SideBar.vue109
-rw-r--r--src/js/components/SideBar/SideBarTabComments.vue98
-rw-r--r--src/js/components/SideBar/SideBarTabConfiguration.vue344
-rw-r--r--src/js/components/SideBar/SideBarTabDateOptions.vue173
-rw-r--r--src/js/components/SideBar/SideBarTabInformation.vue58
-rw-r--r--src/js/components/SideBar/SideBarTabShare.vue211
-rw-r--r--src/js/components/SideBar/SideBarTabTextOptions.vue164
-rw-r--r--src/js/components/VoteTable/VoteHeader.vue77
-rw-r--r--src/js/components/VoteTable/VoteHeaderPublic.vue221
-rw-r--r--src/js/components/VoteTable/VoteTable.vue210
-rw-r--r--src/js/components/VoteTable/VoteTableHeader.vue291
-rw-r--r--src/js/components/VoteTable/VoteTableItem.vue169
-rw-r--r--src/js/main.js70
-rw-r--r--src/js/router.js81
-rw-r--r--src/js/store/index.js55
-rw-r--r--src/js/store/modules/acl.js82
-rw-r--r--src/js/store/modules/comments.js95
-rw-r--r--src/js/store/modules/event.js155
-rw-r--r--src/js/store/modules/locale.js60
-rw-r--r--src/js/store/modules/notification.js62
-rw-r--r--src/js/store/modules/options.js141
-rw-r--r--src/js/store/modules/shares.js153
-rw-r--r--src/js/store/modules/votes.js168
-rw-r--r--src/js/views/PollList.vue181
-rw-r--r--src/js/views/PublicVote.vue134
-rw-r--r--src/js/views/Vote.vue164
-rw-r--r--src/js/views/img/app.svg38
-rw-r--r--src/js/views/img/expired-unvoted-vote.svg14
-rw-r--r--src/js/views/img/expired-voted-vote.svg11
-rw-r--r--src/js/views/img/open-unvoted-vote.svg14
-rw-r--r--src/js/views/img/open-voted-vote.svg11
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
new file mode 100644
index 00000000..84a879bb
--- /dev/null
+++ b/src/js/assets/app.png
Binary files differ
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 &amp;&amp; !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 &amp;&amp; !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>