pixelfed/resources/assets/js/components/partials/PollCard.vue
2022-05-14 19:54:24 +09:30

333 lines
9 KiB
Vue

<template>
<div>
<div class="card shadow-none rounded-0" :class="{ border: showBorder, 'border-top-0': !showBorderTop}">
<div class="card-body">
<div class="media">
<img class="rounded-circle box-shadow mr-2" :src="status.account.avatar" width="32px" height="32px" alt="avatar">
<div class="media-body">
<div class="pl-2 d-flex align-items-top">
<a class="username font-weight-bold text-dark text-decoration-none text-break" href="#">
{{status.account.acct}}
</a>
<span class="px-1 text-lighter">
·
</span>
<a class="font-weight-bold text-lighter" :href="statusUrl(status)">
{{shortTimestamp(status.created_at)}}
</a>
<span class="d-none d-md-block px-1 text-lighter">
·
</span>
<span class="d-none d-md-block px-1 text-primary font-weight-bold">
<i class="fas fa-poll-h"></i> Poll <sup class="text-lighter">BETA</sup>
</span>
<span class="d-none d-md-block px-1 text-lighter">
·
</span>
<span class="d-none d-md-block px-1 text-lighter font-weight-bold">
<span v-if="status.poll.expired">
Closed
</span>
<span v-else>
Closes in {{ shortTimestampAhead(status.poll.expires_at) }}
</span>
</span>
<span class="text-right" style="flex-grow:1;">
<button class="btn btn-link text-dark py-0" type="button" @click="ctxMenu()">
<span class="fas fa-ellipsis-h text-lighter"></span>
<span class="sr-only">Post Menu</span>
</button>
</span>
</div>
<div class="pl-2">
<div class="poll py-3">
<div class="pt-2 text-break d-flex align-items-center mb-3" style="font-size: 17px;">
<span class="btn btn-primary px-2 py-1">
<i class="fas fa-poll-h fa-lg"></i>
</span>
<span class="font-weight-bold ml-3" v-html="status.content"></span>
</div>
<div class="mb-2">
<div v-if="tab === 'vote'">
<p v-for="(option, index) in status.poll.options">
<button
class="btn btn-block lead rounded-pill"
:class="[ index == selectedIndex ? 'btn-primary' : 'btn-outline-primary' ]"
@click="selectOption(index)"
:disabled="!authenticated">
{{ option.title }}
</button>
</p>
<p v-if="selectedIndex != null" class="text-right">
<button class="btn btn-primary btn-sm font-weight-bold px-3" @click="submitVote()">Vote</button>
</p>
</div>
<div v-else-if="tab === 'voted'">
<div v-for="(option, index) in status.poll.options" class="mb-3">
<button
class="btn btn-block lead rounded-pill"
:class="[ index == selectedIndex ? 'btn-primary' : 'btn-outline-secondary' ]"
disabled>
{{ option.title }}
</button>
<div class="font-weight-bold">
<span class="text-muted">{{ calculatePercentage(option) }}%</span>
<span class="small text-lighter">({{option.votes_count}} {{option.votes_count == 1 ? 'vote' : 'votes'}})</span>
</div>
</div>
</div>
<div v-else-if="tab === 'results'">
<div v-for="(option, index) in status.poll.options" class="mb-3">
<button
class="btn btn-outline-secondary btn-block lead rounded-pill"
disabled>
{{ option.title }}
</button>
<div class="font-weight-bold">
<span class="text-muted">{{ calculatePercentage(option) }}%</span>
<span class="small text-lighter">({{option.votes_count}} {{option.votes_count == 1 ? 'vote' : 'votes'}})</span>
</div>
</div>
</div>
</div>
<div>
<p class="mb-0 small text-lighter font-weight-bold d-flex justify-content-between">
<span>{{ status.poll.votes_count }} votes</span>
<a v-if="tab != 'results' && authenticated && !activeRefreshTimeout && status.poll.expired != true && status.poll.voted" class="text-lighter" @click.prevent="refreshResults()" href="#">Refresh Results</a>
<span v-if="tab != 'results' && authenticated && refreshingResults" class="text-lighter">
<div class="spinner-border spinner-border-sm" role="status">
<span class="sr-only">Loading...</span>
</div>
</span>
</p>
</div>
<div>
<span class="d-block d-md-none small text-lighter font-weight-bold">
<span v-if="status.poll.expired">
Closed
</span>
<span v-else>
Closes in {{ shortTimestampAhead(status.poll.expires_at) }}
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<context-menu
ref="contextMenu"
:status="status"
:profile="profile"
v-on:status-delete="statusDeleted"
/>
</div>
</template>
<script type="text/javascript">
import ContextMenu from './ContextMenu.vue';
export default {
props: {
reactions: {
type: Object
},
status: {
type: Object
},
profile: {
type: Object
},
showBorder: {
type: Boolean,
default: true
},
showBorderTop: {
type: Boolean,
default: false
},
fetchState: {
type: Boolean,
default: false
}
},
components: {
"context-menu": ContextMenu
},
data() {
return {
authenticated: false,
tab: 'vote',
selectedIndex: null,
refreshTimeout: undefined,
activeRefreshTimeout: false,
refreshingResults: false
}
},
mounted() {
if(this.fetchState) {
axios.get('/api/v1/polls/' + this.status.poll.id)
.then(res => {
this.status.poll = res.data;
if(res.data.voted) {
this.selectedIndex = res.data.own_votes[0];
this.tab = 'voted';
}
this.status.poll.expired = new Date(this.status.poll.expires_at) < new Date();
if(this.status.poll.expired) {
this.tab = this.status.poll.voted ? 'voted' : 'results';
}
})
} else {
if(this.status.poll.voted) {
this.tab = 'voted';
}
this.status.poll.expired = new Date(this.status.poll.expires_at) < new Date();
if(this.status.poll.expired) {
this.tab = this.status.poll.voted ? 'voted' : 'results';
}
if(this.status.poll.own_votes.length) {
this.selectedIndex = this.status.poll.own_votes[0];
}
}
this.authenticated = $('body').hasClass('loggedIn');
},
methods: {
selectOption(index) {
event.currentTarget.blur();
this.selectedIndex = index;
// if(this.options[index].selected) {
// this.selectedIndex = null;
// this.options[index].selected = false;
// return;
// }
// this.options = this.options.map(o => {
// o.selected = false;
// return o;
// });
// this.options[index].selected = true;
// this.selectedIndex = index;
// this.options[index].score = 100;
},
submitVote() {
// todo: send vote
axios.post('/api/v1/polls/'+this.status.poll.id+'/votes', {
'choices': [
this.selectedIndex
]
}).then(res => {
console.log(res.data);
this.status.poll = res.data;
});
this.tab = 'voted';
},
viewResultsTab() {
this.tab = 'results';
},
viewPollTab() {
this.tab = this.selectedIndex != null ? 'voted' : 'vote';
},
formatCount(count) {
return App.util.format.count(count);
},
statusUrl(status) {
if(status.local == true) {
return status.url;
}
return '/i/web/post/_/' + status.account.id + '/' + status.id;
},
profileUrl(status) {
if(status.local == true) {
return status.account.url;
}
return '/i/web/profile/_/' + status.account.id;
},
timestampFormat(timestamp) {
let ts = new Date(timestamp);
return ts.toDateString() + ' ' + ts.toLocaleTimeString();
},
shortTimestamp(ts) {
return window.App.util.format.timeAgo(ts);
},
shortTimestampAhead(ts) {
return window.App.util.format.timeAhead(ts);
},
refreshResults() {
this.activeRefreshTimeout = true;
this.refreshingResults = true;
axios.get('/api/v1/polls/' + this.status.poll.id)
.then(res => {
this.status.poll = res.data;
if(this.status.poll.voted) {
this.selectedIndex = this.status.poll.own_votes[0];
this.tab = 'voted';
this.setActiveRefreshTimeout();
this.refreshingResults = false;
}
}).catch(err => {
swal('Oops!', 'An error occured while fetching the latest poll results. Please try again later.', 'error');
this.setActiveRefreshTimeout();
this.refreshingResults = false;
});
},
setActiveRefreshTimeout() {
let self = this;
this.refreshTimeout = setTimeout(function() {
self.activeRefreshTimeout = false;
}, 30000);
},
statusDeleted(status) {
this.$emit('status-delete', status);
},
ctxMenu() {
this.$refs.contextMenu.open();
},
likeStatus() {
this.$emit('likeStatus', this.status);
},
calculatePercentage(option) {
let status = this.status;
return status.poll.votes_count == 0 ? 0 : Math.round((option.votes_count / status.poll.votes_count) * 100);
}
}
}
</script>