Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions assets/javascripts/discourse/components/vote-box.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { hash } from "@ember/helper";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import concatClass from "discourse/helpers/concat-class";
import routeAction from "discourse/helpers/route-action";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import closeOnClickOutside from "discourse/modifiers/close-on-click-outside";
import { i18n } from "discourse-i18n";
import VoteButton from "./vote-button";
import VoteCount from "./vote-count";
import VoteOptions from "./vote-options";

export default class VoteBox extends Component {
@service siteSettings;
@service currentUser;

@tracked votesAlert;
@tracked allowClick = true;
@tracked initialVote = false;
@tracked showOptions = false;

@action
addVote() {
let topic = this.args.topic;
return ajax("/voting/vote", {
type: "POST",
data: {
topic_id: topic.id,
},
})
.then((result) => {
topic.vote_count = result.vote_count;
topic.user_voted = true;
this.currentUser.votes_exceeded = !result.can_vote;
this.currentUser.votes_left = result.votes_left;
if (result.alert) {
this.votesAlert = result.votes_left;
}
this.allowClick = true;
this.showOptions = false;
})
.catch(popupAjaxError);
}

@action
removeVote() {
const topic = this.args.topic;

return ajax("/voting/unvote", {
type: "POST",
data: {
topic_id: topic.id,
},
})
.then((result) => {
topic.vote_count = result.vote_count;
topic.user_voted = false;
this.currentUser.votes_exceeded = !result.can_vote;
this.currentUser.votes_left = result.votes_left;
this.allowClick = true;
this.showOptions = false;
})
.catch(popupAjaxError);
}

@action
showVoteOptions() {
this.showOptions = true;
}

@action
closeVoteOptions() {
this.showOptions = false;
}

@action
closeVotesAlert() {
this.votesAlert = null;
}

<template>
<div
class={{concatClass
"voting-wrapper"
(if this.siteSettings.topic_voting_show_who_voted "show-pointer")
}}
>
<VoteCount @topic={{@topic}} @showLogin={{routeAction "showLogin"}} />
<VoteButton
@topic={{@topic}}
@allowClick={{this.allowClick}}
@showVoteOptions={{this.showVoteOptions}}
@addVote={{this.addVote}}
@showLogin={{routeAction "showLogin"}}
/>

{{#if this.showOptions}}
<VoteOptions
@topic={{@topic}}
@removeVote={{this.removeVote}}
{{closeOnClickOutside this.closeVoteOptions (hash)}}
/>
{{/if}}

{{#if this.votesAlert}}
<div
class="voting-popup-menu vote-options popup-menu"
{{closeOnClickOutside this.closeVotesAlert (hash)}}
>
{{htmlSafe
(i18n
"topic_voting.votes_left"
count=this.votesAlert
path="/my/activity/votes"
)
}}
</div>
{{/if}}
</div>
</template>
}
104 changes: 104 additions & 0 deletions assets/javascripts/discourse/components/vote-button.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { service } from "@ember/service";
import DButton from "discourse/components/d-button";
import cookie from "discourse/lib/cookie";
import { applyBehaviorTransformer } from "discourse/lib/transformer";
import { i18n } from "discourse-i18n";

export default class VoteBox extends Component {
@service siteSettings;
@service currentUser;

get wrapperClasses() {
const classes = [];
const { topic } = this.args;
if (topic.closed) {
classes.push("voting-closed");
} else {
if (!topic.user_voted) {
classes.push("nonvote");
} else {
if (this.currentUser && this.currentUser.votes_exceeded) {
classes.push("vote-limited nonvote");
} else {
classes.push("vote");
}
}
}
if (this.siteSettings.topic_voting_show_who_voted) {
classes.push("show-pointer");
}
return classes.join(" ");
}

get buttonContent() {
const { topic } = this.args;
if (this.currentUser) {
if (topic.closed) {
return i18n("topic_voting.voting_closed_title");
}

if (topic.user_voted) {
return i18n("topic_voting.voted_title");
}

if (this.currentUser.votes_exceeded) {
return i18n("topic_voting.voting_limit");
}

return i18n("topic_voting.vote_title");
}

if (topic.vote_count) {
return i18n("topic_voting.anonymous_button", {
count: topic.vote_count,
});
}

return i18n("topic_voting.anonymous_button", { count: 1 });
}

@action
click() {
applyBehaviorTransformer("topic-vote-button-click", () => {
if (!this.currentUser) {
cookie("destination_url", window.location.href, { path: "/" });
this.args.showLogin();
return;
}

const { topic } = this.args;

if (
!topic.closed &&
!topic.user_voted &&
!this.currentUser.votes_exceeded
) {
this.args.addVote();
}

if (topic.user_voted || this.currentUser.votes_exceeded) {
this.args.showVoteOptions();
}
});
}

<template>
<div class={{this.wrapperClasses}}>
<DButton
@translatedTitle={{if
this.currentUser
(i18n
"topic_voting.votes_left_button_title"
count=this.currentUser.votes_left
)
""
}}
@translatedLabel={{this.buttonContent}}
class="btn-primary vote-button"
@action={{this.click}}
/>
</div>
</template>
}
95 changes: 95 additions & 0 deletions assets/javascripts/discourse/components/vote-count.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { hash } from "@ember/helper";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { eq } from "truth-helpers";
import AsyncContent from "discourse/components/async-content";
import SmallUserList from "discourse/components/small-user-list";
import concatClass from "discourse/helpers/concat-class";
import { ajax } from "discourse/lib/ajax";
import cookie from "discourse/lib/cookie";
import { bind } from "discourse/lib/decorators";
import getURL from "discourse/lib/get-url";
import closeOnClickOutside from "discourse/modifiers/close-on-click-outside";

export default class VoteBox extends Component {
@service siteSettings;
@service currentUser;

@tracked showWhoVoted = false;

@bind
async loadWhoVoted() {
return ajax("/voting/who", {
type: "GET",
data: {
topic_id: this.args.topic.id,
},
}).then((users) =>
users.map((user) => {
return {
template: user.avatar_template,
username: user.username,
post_url: user.post_url,
url: getURL("/u/") + user.username.toLowerCase(),
};
})
);
}

@action
click(event) {
event.preventDefault();
event.stopPropagation();

if (!this.currentUser) {
cookie("destination_url", window.location.href, { path: "/" });
this.args.showLogin();
return;
}

if (this.showWhoVoted) {
this.showWhoVoted = false;
} else if (this.siteSettings.topic_voting_show_who_voted) {
this.showWhoVoted = true;
}
}

@action
clickOutside() {
this.showWhoVoted = false;
}

<template>
<div
class={{concatClass
"vote-count-wrapper"
(if (eq @topic.vote_count 0) "no-votes")
}}
{{on "click" this.click}}
role="button"
>
<div class="vote-count">
{{@topic.vote_count}}
</div>
</div>

{{#if this.showWhoVoted}}
<div
class="who-voted popup-menu voting-popup-menu"
{{closeOnClickOutside
this.clickOutside
(hash secondaryTargetSelector=".vote-count-wrapper")
}}
>
<AsyncContent @asyncData={{this.loadWhoVoted}}>
<:content as |voters|>
<SmallUserList @users={{voters}} class="regular-votes" />
</:content>
</AsyncContent>
</div>
{{/if}}
</template>
}
29 changes: 29 additions & 0 deletions assets/javascripts/discourse/components/vote-options.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Component from "@glimmer/component";
import { on } from "@ember/modifier";
import { service } from "@ember/service";
import icon from "discourse/helpers/d-icon";
import { i18n } from "discourse-i18n";

export default class VoteBox extends Component {
@service currentUser;

<template>
<div class="vote-options voting-popup-menu popup-menu" ...attributes>
{{#if @topic.user_voted}}
<div
role="button"
class="remove-vote vote-option"
{{on "click" @removeVote}}
>
{{icon "xmark"}}
{{i18n "topic_voting.remove_vote"}}
</div>
{{else if this.currentUser.votes_exceeded}}
<div>{{i18n "topic_voting.reached_limit"}}</div>
<p>
<a href="/my/activity/votes">{{i18n "topic_voting.list_votes"}}</a>
</p>
{{/if}}
</div>
</template>
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Component from "@ember/component";
import { classNames, tagName } from "@ember-decorators/component";
import MountWidget from "discourse/components/mount-widget";
import routeAction from "discourse/helpers/route-action";
import VoteBox from "../../components/vote-box";

@tagName("div")
@classNames("topic-above-post-stream-outlet", "topic-title-voting")
Expand All @@ -11,10 +11,8 @@ export default class TopicTitleVoting extends Component {
{{#if this.model.postStream.loaded}}
{{#if this.model.postStream.firstPostPresent}}
<div class="voting title-voting">
{{! template-lint-disable no-capital-arguments }}
<MountWidget
@widget="vote-box"
@args={{this.model}}
<VoteBox
@topic={{this.model}}
@showLogin={{routeAction "showLogin"}}
/>
</div>
Expand Down
Loading