]> Untitled Git - lemmy-ui.git/blob - src/shared/components/common/vote-buttons.tsx
Merge branch 'main' into feat/vote-components
[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           className={`btn-animate btn py-0 px-1 ${
110             this.props.my_vote === 1 ? "text-info" : "text-muted"
111           }`}
112           data-tippy-content={tippy(this.props.counts)}
113           onClick={linkEvent(this, handleUpvote)}
114           aria-label={I18NextService.i18n.t("upvote")}
115           aria-pressed={this.props.my_vote === 1}
116         >
117           {this.state.upvoteLoading ? (
118             <Spinner />
119           ) : (
120             <>
121               <Icon icon="arrow-up1" classes="icon-inline small" />
122               {showScores() && (
123                 <span className="ms-2">
124                   {numToSI(this.props.counts.upvotes)}
125                 </span>
126               )}
127             </>
128           )}
129         </button>
130         {this.props.enableDownvotes && (
131           <button
132             className={`ms-2 btn-animate btn py-0 px-1 ${
133               this.props.my_vote === -1 ? "text-danger" : "text-muted"
134             }`}
135             onClick={linkEvent(this, handleDownvote)}
136             data-tippy-content={tippy(this.props.counts)}
137             aria-label={I18NextService.i18n.t("downvote")}
138             aria-pressed={this.props.my_vote === -1}
139           >
140             {this.state.downvoteLoading ? (
141               <Spinner />
142             ) : (
143               <>
144                 <Icon icon="arrow-down1" classes="icon-inline small" />
145                 {showScores() && (
146                   <span
147                     className={classNames("ms-2", {
148                       invisible: this.props.counts.downvotes === 0,
149                     })}
150                   >
151                     {numToSI(this.props.counts.downvotes)}
152                   </span>
153                 )}
154               </>
155             )}
156           </button>
157         )}
158       </div>
159     );
160   }
161 }
162
163 export class VoteButtons extends Component<VoteButtonsProps, VoteButtonsState> {
164   state: VoteButtonsState = {
165     upvoteLoading: false,
166     downvoteLoading: false,
167   };
168
169   constructor(props: any, context: any) {
170     super(props, context);
171   }
172
173   render() {
174     return (
175       <div className={`vote-bar col-1 pe-0 small text-center`}>
176         <button
177           className={`btn-animate btn btn-link p-0 ${
178             this.props.my_vote == 1 ? "text-info" : "text-muted"
179           }`}
180           onClick={linkEvent(this, handleUpvote)}
181           data-tippy-content={I18NextService.i18n.t("upvote")}
182           aria-label={I18NextService.i18n.t("upvote")}
183           aria-pressed={this.props.my_vote === 1}
184         >
185           {this.state.upvoteLoading ? (
186             <Spinner />
187           ) : (
188             <Icon icon="arrow-up1" classes="upvote" />
189           )}
190         </button>
191         {showScores() ? (
192           <div
193             className={`unselectable pointer text-muted px-1 post-score`}
194             data-tippy-content={tippy(this.props.counts)}
195           >
196             {numToSI(this.props.counts.score)}
197           </div>
198         ) : (
199           <div className="p-1"></div>
200         )}
201         {this.props.enableDownvotes && (
202           <button
203             className={`btn-animate btn btn-link p-0 ${
204               this.props.my_vote == -1 ? "text-danger" : "text-muted"
205             }`}
206             onClick={linkEvent(this, handleDownvote)}
207             data-tippy-content={I18NextService.i18n.t("downvote")}
208             aria-label={I18NextService.i18n.t("downvote")}
209             aria-pressed={this.props.my_vote === -1}
210           >
211             {this.state.downvoteLoading ? (
212               <Spinner />
213             ) : (
214               <Icon icon="arrow-down1" classes="downvote" />
215             )}
216           </button>
217         )}
218       </div>
219     );
220   }
221 }