feature/share-libs
tsx 194 lines 5.61 KB
Raw
1 import { BsArrowUpCircleFill, BsArrowDownCircleFill } from "react-icons/bs";
2 import { VoterGravatarsViews } from "./voters-view";
3 import { SongProviderResponse } from "@/models/content";
4 import { CSSProperties, useContext, useState } from "react";
5 import { TAURI_CONTENT_SONG_BRIDGE_VOTE } from "@/lib/tauri-handler";
6 import { TauriResponse } from "@/models/shared";
7 import { invoke } from "@tauri-apps/api/tauri";
8 import { UserContext } from "@//lib/current-user-context";
9 import { useRouter } from "next/navigation";
10 import { CurrentUser } from "@/lib/db";
11 import { useLogger } from "@/lib/logger";
12 import { SongProviderVote } from "@/models/song-votes-detail-response";
13 import { LoadingView } from "@/components/ui/loading-view";
14
15 export enum VoteType {
16 UP = "up",
17 DOWN = "down",
18 }
19
20 type VotePayload = {
21 songProviderId: number;
22 voteType: string;
23 accessToken: string;
24 };
25
26 export enum State {
27 LOADING,
28 LOADED,
29 ERROR,
30 }
31
32 export interface VoteState {
33 aggregate: number;
34 buttonUpStyle: CSSProperties;
35 buttonDownStyle: CSSProperties;
36 upVotes: SongProviderVote[];
37 downVotes: SongProviderVote[];
38 }
39
40 export default function MainVotesView(props: {
41 songProviderId: number;
42 votes: SongProviderVote[];
43 onVotesAggregateUpdate: (votesAggregate: number) => void;
44 }) {
45 const { songProviderId, votes, onVotesAggregateUpdate } = props;
46
47 const log = useLogger("Votes view");
48 const router = useRouter();
49 const [state, setState] = useState(State.LOADED);
50 const [goToLogin, setGoToLogin] = useState(false);
51 const { user } = useContext(UserContext);
52
53 const [voteState, setVoteState] = useState<VoteState>({
54 aggregate: calculateVotes(votes),
55 buttonUpStyle: buttonStyle(votes, VoteType.UP, user),
56 buttonDownStyle: buttonStyle(votes, VoteType.DOWN, user),
57 upVotes: votes.filter((x) => x.vote_type === VoteType.UP),
58 downVotes: votes.filter((x) => x.vote_type === VoteType.DOWN),
59 });
60
61 const vote = async (type: VoteType) => {
62 if (!user || !user.accessToken || user.accessToken.length <= 1) {
63 setGoToLogin(true);
64 return;
65 }
66
67 setState(State.LOADING);
68 try {
69 const payload: VotePayload = {
70 songProviderId,
71 voteType: type,
72 accessToken: user.accessToken,
73 };
74 const response = await invoke<SongProviderResponse>(
75 TAURI_CONTENT_SONG_BRIDGE_VOTE,
76 payload
77 );
78 if (response.status === TauriResponse.SUCCESS) {
79 setVoteState({
80 aggregate: calculateVotes(response.votes),
81 buttonUpStyle: buttonStyle(response.votes, VoteType.UP, user),
82 buttonDownStyle: buttonStyle(response.votes, VoteType.DOWN, user),
83 upVotes: response.votes.filter((x) => x.vote_type === VoteType.UP),
84 downVotes: response.votes.filter(
85 (x) => x.vote_type === VoteType.DOWN
86 ),
87 });
88 onVotesAggregateUpdate(voteState.aggregate);
89 setState(State.LOADED);
90 } else {
91 log.error("Error voting", response);
92 setState(State.ERROR);
93 }
94 } catch (error) {
95 log.error(error);
96 setState(State.ERROR);
97 }
98 };
99
100 if (goToLogin) {
101 router.push("/login");
102 return <></>;
103 }
104
105 return (
106 <div className="w-full flex flex-col sm:flex-row flex-wrap sm:flex-nowrap py-4 flex-grow min-h-10">
107 <div className="w-fixed w-full flex-shrink flex-grow-0 px-4">
108 <VoterGravatarsViews voters={voteState.upVotes} type={VoteType.UP} />
109 </div>
110 <div className="w-full flex-grow pt-1 px-3 flex justify-center">
111 <div className="grid-rows-3 gap-2">
112 {state === State.LOADING && (
113 <LoadingView />
114 )}
115 {state === State.LOADED && (
116 <div>
117 <BsArrowUpCircleFill
118 size={40}
119 className="mb-3"
120 style={voteState.buttonUpStyle}
121 onClick={() => vote(VoteType.UP)}
122 color="green"
123 />
124 <h1 className="flex justify-center">{voteState.aggregate}</h1>
125 <BsArrowDownCircleFill
126 size={40}
127 className="mt-3"
128 style={voteState.buttonDownStyle}
129 onClick={() => vote(VoteType.DOWN)}
130 color="green"
131 />
132 </div>
133 )}
134 </div>
135 </div>
136 <div className="w-fixed w-full flex-shrink flex-grow-0 px-2">
137 <VoterGravatarsViews
138 voters={voteState.downVotes}
139 type={VoteType.DOWN}
140 />
141 </div>
142 </div>
143 );
144 }
145
146 const buttonStyle = (
147 votes: SongProviderVote[],
148 type: VoteType,
149 user: CurrentUser | null
150 ): CSSProperties => {
151 if (!user) {
152 return { cursor: "pointer" };
153 }
154
155 if (type === VoteType.UP) {
156 if (shouldDisableButton(votes, VoteType.UP, user)) {
157 return { pointerEvents: "none", opacity: "0.4" };
158 } else {
159 return { cursor: "pointer" };
160 }
161 } else {
162 if (shouldDisableButton(votes, VoteType.DOWN, user)) {
163 return { pointerEvents: "none", opacity: "0.4" };
164 } else {
165 return { cursor: "pointer" };
166 }
167 }
168 };
169
170 const calculateVotes = (votes: SongProviderVote[]): number => {
171 if (votes.length === 0) {
172 return 0;
173 }
174
175 const up = votes.filter((x) => x.vote_type === VoteType.UP).length;
176 const down = votes.filter((x) => x.vote_type === VoteType.DOWN).length;
177 const votesAggregate = up - down;
178 return votesAggregate;
179 };
180
181 const shouldDisableButton = (
182 votes: SongProviderVote[],
183 type: VoteType,
184 user: CurrentUser
185 ) => {
186 if (!user) {
187 return false;
188 }
189 const x = votes.filter((y) => {
190 if (y.user_id === user.user.id && y.vote_type === type) return true;
191 return false;
192 });
193 return x.length > 0;
194 };