PollCard.vue 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. <template>
  2. <div>
  3. <div class="card shadow-none rounded-0" :class="{ border: showBorder, 'border-top-0': !showBorderTop}">
  4. <div class="card-body">
  5. <div class="media">
  6. <img class="rounded-circle box-shadow mr-2" :src="status.account.avatar" width="32px" height="32px" alt="avatar">
  7. <div class="media-body">
  8. <div class="pl-2 d-flex align-items-top">
  9. <a class="username font-weight-bold text-dark text-decoration-none text-break" href="#">
  10. {{status.account.acct}}
  11. </a>
  12. <span class="px-1 text-lighter">
  13. ·
  14. </span>
  15. <a class="font-weight-bold text-lighter" :href="statusUrl(status)">
  16. {{shortTimestamp(status.created_at)}}
  17. </a>
  18. <span class="d-none d-md-block px-1 text-lighter">
  19. ·
  20. </span>
  21. <span class="d-none d-md-block px-1 text-primary font-weight-bold">
  22. <i class="fas fa-poll-h"></i> Poll <sup class="text-lighter">BETA</sup>
  23. </span>
  24. <span class="d-none d-md-block px-1 text-lighter">
  25. ·
  26. </span>
  27. <span class="d-none d-md-block px-1 text-lighter font-weight-bold">
  28. <span v-if="status.poll.expired">
  29. Closed
  30. </span>
  31. <span v-else>
  32. Closes in {{ shortTimestampAhead(status.poll.expires_at) }}
  33. </span>
  34. </span>
  35. <span class="text-right" style="flex-grow:1;">
  36. <button class="btn btn-link text-dark py-0" type="button" @click="ctxMenu()">
  37. <span class="fas fa-ellipsis-h text-lighter"></span>
  38. <span class="sr-only">Post Menu</span>
  39. </button>
  40. </span>
  41. </div>
  42. <div class="pl-2">
  43. <div class="poll py-3">
  44. <div class="pt-2 text-break d-flex align-items-center mb-3" style="font-size: 17px;">
  45. <span class="btn btn-primary px-2 py-1">
  46. <i class="fas fa-poll-h fa-lg"></i>
  47. </span>
  48. <span class="font-weight-bold ml-3" v-html="status.content"></span>
  49. </div>
  50. <div class="mb-2">
  51. <div v-if="tab === 'vote'">
  52. <p v-for="(option, index) in status.poll.options">
  53. <button
  54. class="btn btn-block lead rounded-pill"
  55. :class="[ index == selectedIndex ? 'btn-primary' : 'btn-outline-primary' ]"
  56. @click="selectOption(index)"
  57. :disabled="!authenticated">
  58. {{ option.title }}
  59. </button>
  60. </p>
  61. <p v-if="selectedIndex != null" class="text-right">
  62. <button class="btn btn-primary btn-sm font-weight-bold px-3" @click="submitVote()">Vote</button>
  63. </p>
  64. </div>
  65. <div v-else-if="tab === 'voted'">
  66. <div v-for="(option, index) in status.poll.options" class="mb-3">
  67. <button
  68. class="btn btn-block lead rounded-pill"
  69. :class="[ index == selectedIndex ? 'btn-primary' : 'btn-outline-secondary' ]"
  70. disabled>
  71. {{ option.title }}
  72. </button>
  73. <div class="font-weight-bold">
  74. <span class="text-muted">{{ calculatePercentage(option) }}%</span>
  75. <span class="small text-lighter">({{option.votes_count}} {{option.votes_count == 1 ? 'vote' : 'votes'}})</span>
  76. </div>
  77. </div>
  78. </div>
  79. <div v-else-if="tab === 'results'">
  80. <div v-for="(option, index) in status.poll.options" class="mb-3">
  81. <button
  82. class="btn btn-outline-secondary btn-block lead rounded-pill"
  83. disabled>
  84. {{ option.title }}
  85. </button>
  86. <div class="font-weight-bold">
  87. <span class="text-muted">{{ calculatePercentage(option) }}%</span>
  88. <span class="small text-lighter">({{option.votes_count}} {{option.votes_count == 1 ? 'vote' : 'votes'}})</span>
  89. </div>
  90. </div>
  91. </div>
  92. </div>
  93. <div>
  94. <p class="mb-0 small text-lighter font-weight-bold d-flex justify-content-between">
  95. <span>{{ status.poll.votes_count }} votes</span>
  96. <a v-if="tab != 'results' && authenticated && !activeRefreshTimeout && status.poll.expired != true && status.poll.voted" class="text-lighter" @click.prevent="refreshResults()" href="#">Refresh Results</a>
  97. <span v-if="tab != 'results' && authenticated && refreshingResults" class="text-lighter">
  98. <div class="spinner-border spinner-border-sm" role="status">
  99. <span class="sr-only">Loading...</span>
  100. </div>
  101. </span>
  102. </p>
  103. </div>
  104. <div>
  105. <span class="d-block d-md-none small text-lighter font-weight-bold">
  106. <span v-if="status.poll.expired">
  107. Closed
  108. </span>
  109. <span v-else>
  110. Closes in {{ shortTimestampAhead(status.poll.expires_at) }}
  111. </span>
  112. </span>
  113. </div>
  114. </div>
  115. </div>
  116. </div>
  117. </div>
  118. </div>
  119. </div>
  120. <context-menu
  121. ref="contextMenu"
  122. :status="status"
  123. :profile="profile"
  124. v-on:status-delete="statusDeleted"
  125. />
  126. </div>
  127. </template>
  128. <script type="text/javascript">
  129. import ContextMenu from './ContextMenu.vue';
  130. export default {
  131. props: {
  132. reactions: {
  133. type: Object
  134. },
  135. status: {
  136. type: Object
  137. },
  138. profile: {
  139. type: Object
  140. },
  141. showBorder: {
  142. type: Boolean,
  143. default: true
  144. },
  145. showBorderTop: {
  146. type: Boolean,
  147. default: false
  148. },
  149. fetchState: {
  150. type: Boolean,
  151. default: false
  152. }
  153. },
  154. components: {
  155. "context-menu": ContextMenu
  156. },
  157. data() {
  158. return {
  159. authenticated: false,
  160. tab: 'vote',
  161. selectedIndex: null,
  162. refreshTimeout: undefined,
  163. activeRefreshTimeout: false,
  164. refreshingResults: false
  165. }
  166. },
  167. mounted() {
  168. if(this.fetchState) {
  169. axios.get('/api/v1/polls/' + this.status.poll.id)
  170. .then(res => {
  171. this.status.poll = res.data;
  172. if(res.data.voted) {
  173. this.selectedIndex = res.data.own_votes[0];
  174. this.tab = 'voted';
  175. }
  176. this.status.poll.expired = new Date(this.status.poll.expires_at) < new Date();
  177. if(this.status.poll.expired) {
  178. this.tab = this.status.poll.voted ? 'voted' : 'results';
  179. }
  180. })
  181. } else {
  182. if(this.status.poll.voted) {
  183. this.tab = 'voted';
  184. }
  185. this.status.poll.expired = new Date(this.status.poll.expires_at) < new Date();
  186. if(this.status.poll.expired) {
  187. this.tab = this.status.poll.voted ? 'voted' : 'results';
  188. }
  189. if(this.status.poll.own_votes.length) {
  190. this.selectedIndex = this.status.poll.own_votes[0];
  191. }
  192. }
  193. this.authenticated = $('body').hasClass('loggedIn');
  194. },
  195. methods: {
  196. selectOption(index) {
  197. event.currentTarget.blur();
  198. this.selectedIndex = index;
  199. // if(this.options[index].selected) {
  200. // this.selectedIndex = null;
  201. // this.options[index].selected = false;
  202. // return;
  203. // }
  204. // this.options = this.options.map(o => {
  205. // o.selected = false;
  206. // return o;
  207. // });
  208. // this.options[index].selected = true;
  209. // this.selectedIndex = index;
  210. // this.options[index].score = 100;
  211. },
  212. submitVote() {
  213. // todo: send vote
  214. axios.post('/api/v1/polls/'+this.status.poll.id+'/votes', {
  215. 'choices': [
  216. this.selectedIndex
  217. ]
  218. }).then(res => {
  219. console.log(res.data);
  220. this.status.poll = res.data;
  221. });
  222. this.tab = 'voted';
  223. },
  224. viewResultsTab() {
  225. this.tab = 'results';
  226. },
  227. viewPollTab() {
  228. this.tab = this.selectedIndex != null ? 'voted' : 'vote';
  229. },
  230. formatCount(count) {
  231. return App.util.format.count(count);
  232. },
  233. statusUrl(status) {
  234. if(status.local == true) {
  235. return status.url;
  236. }
  237. return '/i/web/post/_/' + status.account.id + '/' + status.id;
  238. },
  239. profileUrl(status) {
  240. if(status.local == true) {
  241. return status.account.url;
  242. }
  243. return '/i/web/profile/_/' + status.account.id;
  244. },
  245. timestampFormat(timestamp) {
  246. let ts = new Date(timestamp);
  247. return ts.toDateString() + ' ' + ts.toLocaleTimeString();
  248. },
  249. shortTimestamp(ts) {
  250. return window.App.util.format.timeAgo(ts);
  251. },
  252. shortTimestampAhead(ts) {
  253. return window.App.util.format.timeAhead(ts);
  254. },
  255. refreshResults() {
  256. this.activeRefreshTimeout = true;
  257. this.refreshingResults = true;
  258. axios.get('/api/v1/polls/' + this.status.poll.id)
  259. .then(res => {
  260. this.status.poll = res.data;
  261. if(this.status.poll.voted) {
  262. this.selectedIndex = this.status.poll.own_votes[0];
  263. this.tab = 'voted';
  264. this.setActiveRefreshTimeout();
  265. this.refreshingResults = false;
  266. }
  267. }).catch(err => {
  268. swal('Oops!', 'An error occured while fetching the latest poll results. Please try again later.', 'error');
  269. this.setActiveRefreshTimeout();
  270. this.refreshingResults = false;
  271. });
  272. },
  273. setActiveRefreshTimeout() {
  274. let self = this;
  275. this.refreshTimeout = setTimeout(function() {
  276. self.activeRefreshTimeout = false;
  277. }, 30000);
  278. },
  279. statusDeleted(status) {
  280. this.$emit('status-delete', status);
  281. },
  282. ctxMenu() {
  283. this.$refs.contextMenu.open();
  284. },
  285. likeStatus() {
  286. this.$emit('likeStatus', this.status);
  287. },
  288. calculatePercentage(option) {
  289. let status = this.status;
  290. return status.poll.votes_count == 0 ? 0 : Math.round((option.votes_count / status.poll.votes_count) * 100);
  291. }
  292. }
  293. }
  294. </script>