Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an option for polls to allow users to vote for multiple items #69

Merged
merged 11 commits into from
Jun 6, 2023
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
}
],
"require": {
"flarum/core": "^1.2.0"
"flarum/core": "^1.3.0"
},
"replace": {
"reflar/polls": "^1.3.4"
Expand Down
3 changes: 2 additions & 1 deletion extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
(new Extend\Routes('api'))
->patch('/fof/polls/{id}', 'fof.polls.edit', Controllers\EditPollController::class)
->delete('/fof/polls/{id}', 'fof.polls.delete', Controllers\DeletePollController::class)
->patch('/fof/polls/{id}/vote', 'fof.polls.vote', Controllers\VotePollController::class),
->patch('/fof/polls/{id}/vote', 'fof.polls.vote', Controllers\VotePollController::class)
->patch('/fof/polls/{id}/votes', 'fof.polls.votes', Controllers\MultipleVotesPollController::class),

(new Extend\Model(Discussion::class))
->hasOne('poll', Poll::class, 'discussion_id', 'id'),
Expand Down
20 changes: 12 additions & 8 deletions js/src/forum/addPollToDiscussion.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default () => {
app.pusher.then((binding) => {
// We will listen for updates to all polls and options
// Even if that model is not in the current discussion, it doesn't really matter
binding.channels.main.bind('updatedPollOption', (data) => {
binding.channels.main.bind('updatedPollOptions', (data) => {
const poll = app.store.getById('polls', data['pollId']);

if (poll) {
Expand All @@ -63,15 +63,19 @@ export default () => {
// Not redrawing here, as the option below should trigger the redraw already
}

const option = app.store.getById('poll_options', data['optionId']);
const changedOptions = data['options'];

if (option) {
option.pushAttributes({
voteCount: data['optionVoteCount'],
});
for (const optionId in changedOptions) {
const option = app.store.getById('poll_options', optionId);

m.redraw();
if (option) {
option.pushAttributes({
voteCount: changedOptions[optionId],
});
}
}

m.redraw();
});
});
}
Expand All @@ -80,7 +84,7 @@ export default () => {
extend(DiscussionPage.prototype, 'onremove', function () {
if (app.pusher) {
app.pusher.then((binding) => {
binding.channels.main.unbind('updatedPollOption');
binding.channels.main.unbind('updatedPollOptions');
});
}
});
Expand Down
33 changes: 32 additions & 1 deletion js/src/forum/components/CreatePollModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export default class CreatePollModal extends Modal {
this.endDate = Stream();

this.publicPoll = Stream(false);
this.allowMultipleVotes = Stream(false);
this.maxVotes = Stream(0);

const { poll } = this.attrs;

Expand All @@ -42,7 +44,7 @@ export default class CreatePollModal extends Modal {
}

className() {
return 'PollDiscussionModal Modal--small';
return 'PollDiscussionModal Modal--medium';
}

configDatePicker(vnode) {
Expand Down Expand Up @@ -127,6 +129,34 @@ export default class CreatePollModal extends Modal {
20
);

items.add(
'allow-multiple-votes',
<div className="Form-group">
{Switch.component(
{
state: this.allowMultipleVotes() || false,
onchange: this.allowMultipleVotes,
},
app.translator.trans('fof-polls.forum.modal.allow_multiple_votes_label')
)}
</div>,
15
);

if (this.allowMultipleVotes()) {
items.add(
'max-votes',
<div className="Form-group">
<label className="label">{app.translator.trans('fof-polls.forum.modal.max_votes_label')}</label>

<input type="number" min="0" max={this.options.length} name="maxVotes" className="FormControl" bidi={this.maxVotes} />

<p className="helpText">{app.translator.trans('fof-polls.forum.modal.max_votes_help')}</p>
</div>,
15
);
}

items.add(
'submit',
<div className="Form-group">
Expand Down Expand Up @@ -200,6 +230,7 @@ export default class CreatePollModal extends Modal {
question: this.question(),
endDate: this.endDate(),
publicPoll: this.publicPoll(),
allowMultipleVotes: this.allowMultipleVotes(),
options: [],
};

Expand Down
113 changes: 74 additions & 39 deletions js/src/forum/components/DiscussionPoll.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export default class DiscussionPoll extends Component {
}

view() {
let maxVotes = this.poll.allowMultipleVotes() ? this.poll.maxVotes() : 1;

if (maxVotes === 0) maxVotes = this.options.length;

return (
<div>
<h3>{this.poll.question()}</h3>
Expand All @@ -34,18 +38,33 @@ export default class DiscussionPoll extends Component {
)
: ''}

{app.session.user && !app.session.user.canVotePolls() ? (
<div className="helpText PollInfoText">{app.translator.trans('fof-polls.forum.no_permission')}</div>
) : this.poll.hasEnded() ? (
<div className="helpText PollInfoText">{app.translator.trans('fof-polls.forum.poll_ended')}</div>
) : this.poll.endDate() !== null ? (
<div className="helpText PollInfoText">
<i class="icon fas fa-clock-o" />
{app.translator.trans('fof-polls.forum.days_remaining', { time: dayjs(this.poll.endDate()).fromNow() })}
</div>
) : (
''
)}
<div className="helpText PollInfoText">
{app.session.user && !app.session.user.canVotePolls() && (
<span>
<i className="icon fas fa-times-circle" />
{app.translator.trans('fof-polls.forum.no_permission')}
</span>
)}
{this.poll.hasEnded() && (
<span>
<i class="icon fas fa-clock" />
{app.translator.trans('fof-polls.forum.poll_ended')}
</span>
)}
{this.poll.endDate() !== null && (
<span>
<i class="icon fas fa-clock" />
{app.translator.trans('fof-polls.forum.days_remaining', { time: dayjs(this.poll.endDate()).fromNow() })}
</span>
)}

{app.session.user?.canVotePolls() && (
<span>
<i className="icon fas fa-poll" />
{app.translator.trans('fof-polls.forum.max_votes_allowed', { max: maxVotes })}
</span>
)}
</div>
</div>
);
}
Expand All @@ -58,29 +77,31 @@ export default class DiscussionPoll extends Component {
const votes = opt.voteCount();
const percent = totalVotes > 0 ? Math.round((votes / totalVotes) * 100) : 0;

const poll = (
<div className="PollBar" data-selected={voted}>
{((!this.poll.hasEnded() && app.session.user && app.session.user.canVotePolls()) || !app.session.user) && (
<label className="checkbox">
<input onchange={this.changeVote.bind(this, opt)} type="checkbox" checked={voted} disabled={hasVoted && !this.poll.canChangeVote()} />
<span className="checkmark" />
</label>
)}

<div style={!isNaN(votes) && '--width: ' + percent + '%'} className="PollOption-active" />
<label className="PollAnswer">
<span>{opt.answer()}</span>
{opt.imageUrl() ? <img className="PollAnswerImage" src={opt.imageUrl()} alt={opt.answer()} /> : null}
</label>
{!isNaN(votes) && (
<label>
<span className={classList('PollPercent', percent !== 100 && 'PollPercent--option')}>{percent}%</span>
</label>
)}
</div>
);

return (
<div className={classList('PollOption', hasVoted && 'PollVoted', this.poll.hasEnded() && 'PollEnded')}>
<Tooltip text={app.translator.trans('fof-polls.forum.tooltip.votes', { count: votes })}>
<div className="PollBar" data-selected={voted}>
{((!this.poll.hasEnded() && app.session.user && app.session.user.canVotePolls()) || !app.session.user) && (
<label className="checkbox">
<input onchange={this.changeVote.bind(this, opt)} type="checkbox" checked={voted} disabled={hasVoted && !this.poll.canChangeVote()} />
<span className="checkmark" />
</label>
)}

<div style={!isNaN(votes) && '--width: ' + percent + '%'} className="PollOption-active" />
<label className="PollAnswer">
<span>{opt.answer()}</span>
{opt.imageUrl() ? <img className="PollAnswerImage" src={opt.imageUrl()} alt={opt.answer()} /> : null}
</label>
{!isNaN(votes) && (
<label>
<span className={classList('PollPercent', percent !== 100 && 'PollPercent--option')}>{percent}%</span>
</label>
)}
</div>
</Tooltip>
{!isNaN(votes) ? <Tooltip text={app.translator.trans('fof-polls.forum.tooltip.votes', { count: votes })}>{poll}</Tooltip> : poll}
</div>
);
}
Expand All @@ -103,17 +124,28 @@ export default class DiscussionPoll extends Component {
return;
}

// if we click on our current vote, we want to "un-vote"
if (this.myVotes.some((vote) => vote.option() === option)) option = null;
// // if we click on our current vote, we want to "un-vote"
// if (this.myVotes.some((vote) => vote.option() === option)) option = null;

const optionIds = new Set(this.poll.myVotes().map((v) => v.option().id()));
const isUnvoting = optionIds.delete(option.id());
const allowsMultiple = this.poll.allowMultipleVotes();

if (!allowsMultiple) {
optionIds.clear();
}

app
if (!isUnvoting) {
optionIds.add(option.id());
}

return app
.request({
method: 'PATCH',
url: `${app.forum.attribute('apiUrl')}/fof/polls/${this.poll.id()}/vote`,
errorHandler: this.onError.bind(this, evt),
url: `${app.forum.attribute('apiUrl')}/fof/polls/${this.poll.id()}/votes`,
body: {
data: {
optionId: option ? option.id() : null,
optionIds: Array.from(optionIds),
},
},
})
Expand All @@ -123,6 +155,9 @@ export default class DiscussionPoll extends Component {
this.updateData();

m.redraw();
})
.catch(() => {
evt.target.checked = isUnvoting;
});
}

Expand Down
4 changes: 4 additions & 0 deletions js/src/forum/components/EditPollModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export default class EditPollModal extends CreatePollModal {
this.question = Stream(this.poll.question());
this.endDate = Stream(this.poll.endDate());
this.publicPoll = Stream(this.poll.publicPoll());
this.allowMultipleVotes = Stream(this.poll.allowMultipleVotes());
this.maxVotes = Stream(this.poll.maxVotes() || 0);
}

title() {
Expand Down Expand Up @@ -89,6 +91,8 @@ export default class EditPollModal extends CreatePollModal {
question: this.question(),
endDate: this.endDate() || false,
publicPoll: this.publicPoll(),
allowMultipleVotes: this.allowMultipleVotes(),
maxVotes: this.maxVotes(),
options,
};
}
Expand Down
6 changes: 1 addition & 5 deletions js/src/forum/components/ListVotersModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,7 @@ export default class ListVotersModal extends Modal {
<div>
<h2>{opt.answer() + ':'}</h2>

{votes.length ? (
votes.map(this.voteContent.bind(this))
) : (
<h4 style="color: #000">{app.translator.trans('fof-polls.forum.modal.no_voters')}</h4>
)}
{votes.length ? votes.map(this.voteContent.bind(this)) : <h4>{app.translator.trans('fof-polls.forum.modal.no_voters')}</h4>}
</div>
);
}
Expand Down
4 changes: 4 additions & 0 deletions js/src/forum/models/Poll.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ export default class Poll extends Model {
hasEnded = Model.attribute('hasEnded');
endDate = Model.attribute('endDate');
publicPoll = Model.attribute('publicPoll');
allowMultipleVotes = Model.attribute('allowMultipleVotes');
maxVotes = Model.attribute('maxVotes');

voteCount = Model.attribute('voteCount');

canEdit = Model.attribute('canEdit');
canDelete = Model.attribute('canDelete');
canSeeVotes = Model.attribute('canSeeVotes');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

/*
* This file is part of fof/polls.
*
* Copyright (c) FriendsOfFlarum.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

use Flarum\Database\Migration;

return Migration::addColumns('polls', [
'allow_multiple_votes' => ['boolean', 'default' => false],
]);
16 changes: 16 additions & 0 deletions migrations/2023_06_05_000000_add_max_votes_option.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

/*
* This file is part of fof/polls.
*
* Copyright (c) FriendsOfFlarum.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

use Flarum\Database\Migration;

return Migration::addColumns('polls', [
'max_votes' => ['integer', 'unsigned' => true, 'default' => 0],
]);
14 changes: 13 additions & 1 deletion resources/less/forum.less
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,12 @@
padding: 0;
margin: 0;

h4 {
color: var(--text-color);
}

a {
color: @text-color;
color: var(--text-color);
font-size: 15px;
font-weight: bold;
display: block;
Expand All @@ -267,6 +271,14 @@

.PollInfoText {
margin-left: 15px;

span {
display: block;
}

.icon {
width: 15px;
}
}

@keyframes slideIn {
Expand Down
Loading