| 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 | }; |