]> Untitled Git - lemmy-ui.git/blob - src/shared/components/common/vote-buttons.tsx
Merge remote-tracking branch 'lemmy/main' into fix/wider-max-width-1536
[lemmy-ui.git] / src / shared / components / common / vote-buttons.tsx
1 import { myAuthRequired, newVote, showScores } from "@utils/app";
2 import { numToSI } from "@utils/helpers";
3 import classNames from "classnames";
4 import { Component, linkEvent } from "inferno";
5 import {
6   CommentAggregates,
7   CreateCommentLike,
8   CreatePostLike,
9   PostAggregates,
10 } from "lemmy-js-client";
11 import { VoteContentType, VoteType } from "../../interfaces";
12 import { I18NextService } from "../../services";
13 import { Icon, Spinner } from "../common/icon";
14
15 interface VoteButtonsProps {
16   voteContentType: VoteContentType;
17   id: number;
18   onVote: (i: CreateCommentLike | CreatePostLike) => void;
19   enableDownvotes?: boolean;
20   counts: CommentAggregates | PostAggregates;
21   my_vote?: number;
22 }
23
24 interface VoteButtonsState {
25   upvoteLoading: boolean;
26   downvoteLoading: boolean;
27 }
28
29 const tippy = (counts: CommentAggregates | PostAggregates): string => {
30   const points = I18NextService.i18n.t("number_of_points", {
31     count: Number(counts.score),
32     formattedCount: Number(counts.score),
33   });
34
35   const upvotes = I18NextService.i18n.t("number_of_upvotes", {
36     count: Number(counts.upvotes),
37     formattedCount: Number(counts.upvotes),
38   });
39
40   const downvotes = I18NextService.i18n.t("number_of_downvotes", {
41     count: Number(counts.downvotes),
42     formattedCount: Number(counts.downvotes),
43   });
44
45   return `${points} • ${upvotes} • ${downvotes}`;
46 };
47
48 const handleUpvote = (i: VoteButtons) => {
49   i.setState({ upvoteLoading: true });
50
51   switch (i.props.voteContentType) {
52     case VoteContentType.Comment:
53       i.props.onVote({
54         comment_id: i.props.id,
55         score: newVote(VoteType.Upvote, i.props.my_vote),
56         auth: myAuthRequired(),
57       });
58       break;
59     case VoteContentType.Post:
60     default:
61       i.props.onVote({
62         post_id: i.props.id,
63         score: newVote(VoteType.Upvote, i.props.my_vote),
64         auth: myAuthRequired(),
65       });
66   }
67
68   i.setState({ upvoteLoading: false });
69 };
70
71 const handleDownvote = (i: VoteButtons) => {
72   i.setState({ downvoteLoading: true });
73   switch (i.props.voteContentType) {
74     case VoteContentType.Comment:
75       i.props.onVote({
76         comment_id: i.props.id,
77         score: newVote(VoteType.Downvote, i.props.my_vote),
78         auth: myAuthRequired(),
79       });
80       break;
81     case VoteContentType.Post:
82     default:
83       i.props.onVote({
84         post_id: i.props.id,
85         score: newVote(VoteType.Downvote, i.props.my_vote),
86         auth: myAuthRequired(),
87       });
88   }
89   i.setState({ downvoteLoading: false });
90 };
91
92 export class VoteButtonsCompact extends Component<
93   VoteButtonsProps,
94   VoteButtonsState
95 > {
96   state: VoteButtonsState = {
97     upvoteLoading: false,
98     downvoteLoading: false,
99   };
100
101   constructor(props: any, context: any) {
102     super(props, context);
103   }
104
105   render() {
106     return (
107       <div>
108         <button
109           type="button"
110           className={`btn-animate btn py-0 px-1 ${
111             this.props.my_vote === 1 ? "text-info" : "text-muted"
112           }`}
113           data-tippy-content={tippy(this.props.counts)}
114           onClick={linkEvent(this, handleUpvote)}
115           aria-label={I18NextService.i18n.t("upvote")}
116           aria-pressed={this.props.my_vote === 1}
117         >
118           {this.state.upvoteLoading ? (
119             <Spinner />
120           ) : (
121             <>
122               <Icon icon="arrow-up1" classes="icon-inline small" />
123               {showScores() && (
124                 <span className="ms-2">
125                   {numToSI(this.props.counts.upvotes)}
126                 </span>
127               )}
128             </>
129           )}
130         </button>
131         {this.props.enableDownvotes && (
132           <button
133             type="button"
134             className={`ms-2 btn-animate btn py-0 px-1 ${
135               this.props.my_vote === -1 ? "text-danger" : "text-muted"
136             }`}
137             onClick={linkEvent(this, handleDownvote)}
138             data-tippy-content={tippy(this.props.counts)}
139             aria-label={I18NextService.i18n.t("downvote")}
140             aria-pressed={this.props.my_vote === -1}
141           >
142             {this.state.downvoteLoading ? (
143               <Spinner />
144             ) : (
145               <>
146                 <Icon icon="arrow-down1" classes="icon-inline small" />
147                 {showScores() && (
148                   <span
149                     className={classNames("ms-2", {
150                       invisible: this.props.counts.downvotes === 0,
151                     })}
152                   >
153                     {numToSI(this.props.counts.downvotes)}
154                   </span>
155                 )}
156               </>
157             )}
158           </button>
159         )}
160       </div>
161     );
162   }
163 }
164
165 export class VoteButtons extends Component<VoteButtonsProps, VoteButtonsState> {
166   state: VoteButtonsState = {
167     upvoteLoading: false,
168     downvoteLoading: false,
169   };
170
171   constructor(props: any, context: any) {
172     super(props, context);
173   }
174
175   render() {
176     return (
177       <div className="vote-bar pe-0 small text-center">
178         <button
179           type="button"
180           className={`btn-animate btn btn-link p-0 ${
181             this.props.my_vote == 1 ? "text-info" : "text-muted"
182           }`}
183           onClick={linkEvent(this, handleUpvote)}
184           data-tippy-content={I18NextService.i18n.t("upvote")}
185           aria-label={I18NextService.i18n.t("upvote")}
186           aria-pressed={this.props.my_vote === 1}
187         >
188           {this.state.upvoteLoading ? (
189             <Spinner />
190           ) : (
191             <Icon icon="arrow-up1" classes="upvote" />
192           )}
193         </button>
194         {showScores() ? (
195           <div
196             className="unselectable pointer text-muted px-1 post-score"
197             data-tippy-content={tippy(this.props.counts)}
198           >
199             {numToSI(this.props.counts.score)}
200           </div>
201         ) : (
202           <div className="p-1"></div>
203         )}
204         {this.props.enableDownvotes && (
205           <button
206             type="button"
207             className={`btn-animate btn btn-link p-0 ${
208               this.props.my_vote == -1 ? "text-danger" : "text-muted"
209             }`}
210             onClick={linkEvent(this, handleDownvote)}
211             data-tippy-content={I18NextService.i18n.t("downvote")}
212             aria-label={I18NextService.i18n.t("downvote")}
213             aria-pressed={this.props.my_vote === -1}
214           >
215             {this.state.downvoteLoading ? (
216               <Spinner />
217             ) : (
218               <Icon icon="arrow-down1" classes="downvote" />
219             )}
220           </button>
221         )}
222       </div>
223     );
224   }
225 }