main votes view

Seto Elkahfi committed Aug 3, 2024 at 01:55 UTC 9259bc17cda6e6ee77d438a07a76151d24b26425
4 files changed +193 -177
frontend/splitfire-desktop/app/split/_components/button-split.tsx
+2 -2
@@ -27,7 +27,7 @@ export default function ButtonGenerateBackingTracks({
27 audioFile: AudioFile | null;
28 aggregateVotes: number;
29 }) {
30 -
30 +
31 const [state, setState] = useState(State.LOADED);
32 const { user } = useContext(UserContext);
33 const [goToLogin, setGoToLogin] = useState(false);
@@ -117,7 +117,7 @@ export default function ButtonGenerateBackingTracks({
117 }
118
119 return (
120 - <div className="mb-3 mt-3" title="Not enough votes to generate backing tracks...">
120 + <div className="" title="Not enough votes to generate backing tracks...">
121 <CountdownTimerIcon width={40} height={40} className="mb-3" color="red" />
122 </div>
123 );
frontend/splitfire-desktop/app/split/_components/song-votes.tsx
+2 -2
@@ -2,7 +2,7 @@
2
3 import { SongProvider } from "@/app/_src/models/SongResponse";
4 import { SongProviderVote } from "@/app/_src/models/SongVotesDetailResponse";
5 -import UpDownVotesView from "./votes-view";
5 +import VotesView from "./votes-view";
6 import SongVotesImage from "./song-votes-image";
7
8 export default function SongVotes({
@@ -15,7 +15,7 @@ export default function SongVotes({
15 return (
16 <>
17 <h1 className="my-2">{songProvider.name}</h1>
18 - <UpDownVotesView
18 + <VotesView
19 votes={votes}
20 songProviderId={songProvider.id}
21 audioFile={songProvider.audio_file}
frontend/splitfire-desktop/app/split/_components/votes-view-main.tsx
+179 -25
@@ -1,43 +1,197 @@
1 import { BsArrowUpCircleFill, BsArrowDownCircleFill } from "react-icons/bs";
2 -import { VoteState, VoteType } from "./votes-view";
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 "@/app/_src/lib/tauriHandler";
6 +import { SongProviderVote } from "@/app/_src/models/SongVotesDetailResponse";
7 +import { TauriResponse } from "@/models/shared";
8 +import { invoke } from "@tauri-apps/api";
9 +import { useLogger } from "@/app/_src/lib/logger";
10 +import { UserContext } from "@/app/_src/lib/CurrentUserContext";
11 +import { useRouter } from "next/navigation";
12 +import { CurrentUser } from "@/app/_src/lib/db";
13 +import { Spinner } from "react-bootstrap";
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: {
6 - voteState: VoteState;
7 - vote: (type: VoteType) => void;
41 + songProviderId: number;
42 + votes: SongProviderVote[];
43 + onVotesAggregateUpdate: (votesAggregate: number) => void;
44 }) {
9 - const { voteState, vote } = props;
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 (
12 - <div className="w-full flex flex-col sm:flex-row flex-wrap sm:flex-nowrap py-4 flex-grow">
13 - <div className="w-fixed w-full flex-shrink flex-grow-0 px-4 ">
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">
18 - <BsArrowUpCircleFill
19 - size={40}
20 - className="mb-3"
21 - style={voteState.buttonUpStyle}
22 - onClick={() => vote(VoteType.UP)}
23 - color="green"
24 - />
25 - <h1 className="flex justify-center">{voteState.aggregate}</h1>
26 - <BsArrowDownCircleFill
27 - size={40}
28 - className="mt-3"
29 - style={voteState.buttonDownStyle}
30 - onClick={() => vote(VoteType.DOWN)}
31 - color="green"
32 - />
112 + {state === State.LOADING && (
113 + <div className="min-h-80">
114 + <Spinner animation="border" role="status">
115 + </Spinner>
116 + </div>
117 + )}
118 + {state === State.LOADED && (
119 + <div>
120 + <BsArrowUpCircleFill
121 + size={40}
122 + className="mb-3"
123 + style={voteState.buttonUpStyle}
124 + onClick={() => vote(VoteType.UP)}
125 + color="green"
126 + />
127 + <h1 className="flex justify-center">{voteState.aggregate}</h1>
128 + <BsArrowDownCircleFill
129 + size={40}
130 + className="mt-3"
131 + style={voteState.buttonDownStyle}
132 + onClick={() => vote(VoteType.DOWN)}
133 + color="green"
134 + />
135 + </div>
136 + )}
137 </div>
138 </div>
139 <div className="w-fixed w-full flex-shrink flex-grow-0 px-2">
36 - <VoterGravatarsViews
37 - voters={voteState.downVotes}
38 - type={VoteType.DOWN}
39 - />
140 + <VoterGravatarsViews
141 + voters={voteState.downVotes}
142 + type={VoteType.DOWN}
143 + />
144 </div>
145 </div>
146 );
147 }
148 +
149 +const buttonStyle = (
150 + votes: SongProviderVote[],
151 + type: VoteType,
152 + user: CurrentUser | null
153 +): CSSProperties => {
154 + if (!user) {
155 + return { cursor: "pointer" };
156 + }
157 +
158 + if (type === VoteType.UP) {
159 + if (shouldDisableButton(votes, VoteType.UP, user)) {
160 + return { pointerEvents: "none", opacity: "0.4" };
161 + } else {
162 + return { cursor: "pointer" };
163 + }
164 + } else {
165 + if (shouldDisableButton(votes, VoteType.DOWN, user)) {
166 + return { pointerEvents: "none", opacity: "0.4" };
167 + } else {
168 + return { cursor: "pointer" };
169 + }
170 + }
171 +};
172 +
173 +const calculateVotes = (votes: SongProviderVote[]): number => {
174 + if (votes.length === 0) {
175 + return 0;
176 + }
177 +
178 + const up = votes.filter((x) => x.vote_type === VoteType.UP).length;
179 + const down = votes.filter((x) => x.vote_type === VoteType.DOWN).length;
180 + const votesAggregate = up - down;
181 + return votesAggregate;
182 +};
183 +
184 +const shouldDisableButton = (
185 + votes: SongProviderVote[],
186 + type: VoteType,
187 + user: CurrentUser
188 +) => {
189 + if (!user) {
190 + return false;
191 + }
192 + const x = votes.filter((y) => {
193 + if (y.user_id === user.user.id && y.vote_type === type) return true;
194 + return false;
195 + });
196 + return x.length > 0;
197 +};
frontend/splitfire-desktop/app/split/_components/votes-view.tsx
+10 -148
@@ -1,54 +1,19 @@
1 "use client";
2
3 -import {
4 - HTTPStatusCode,
5 - SongBridgeResponse,
6 -} from "@/app/_src/components/pages/esef/SplitFireView";
3 import {
4 AudioFile,
5 Status,
6 } from "@/app/_src/components/player/models/AudioFile";
7 import { UserContext } from "@/app/_src/lib/CurrentUserContext";
12 -import { CurrentUser } from "@/app/_src/lib/db";
8 import { SongProviderVote } from "@/app/_src/models/SongVotesDetailResponse";
9 import { useRouter } from "next/navigation";
15 -import { CSSProperties, useState, useContext } from "react";
16 -import { Spinner } from "react-bootstrap";
10 +import { useState, useContext } from "react";
11 import ButtonGenerateBackingTracks from "./button-split";
18 -import MainVotesView from "./votes-view-main";
12 +import MainVotesView, { } from "./votes-view-main";
13 import LetsPlayView from "./lets-play";
20 -import { invoke } from "@tauri-apps/api";
21 -import { TAURI_CONTENT_SONG_BRIDGE_VOTE } from "@/app/_src/lib/tauriHandler";
14 import { useLogger } from "@/app/_src/lib/logger";
23 -import { SongProviderResponse } from "@/models/content";
24 -import { TauriResponse } from "@/models/shared";
25 -
26 -enum State {
27 - LOADING,
28 - LOADED,
29 - ERROR,
30 -}
31 -
32 -export enum VoteType {
33 - UP = "up",
34 - DOWN = "down",
35 -}
36 -
37 -export interface VoteState {
38 - aggregate: number;
39 - buttonUpStyle: CSSProperties;
40 - buttonDownStyle: CSSProperties;
41 - upVotes: SongProviderVote[];
42 - downVotes: SongProviderVote[];
43 -}
44 -
45 -type VotePayload = {
46 - songProviderId: number;
47 - voteType: string;
48 - accessToken: string;
49 -}
15
51 -export default function UpDownVotesView({
16 +export default function VotesView({
17 songProviderId,
18 votes,
19 audioFile,
@@ -57,137 +22,34 @@ export default function UpDownVotesView({
22 votes: SongProviderVote[];
23 audioFile: AudioFile | null;
24 }) {
25 +
26 const log = useLogger("Votes view");
61 - const [state, setState] = useState(State.LOADED);
62 - const [goToLogin, setGoToLogin] = useState(false);
27 const [goToPlayer, setGoToPlayer] = useState(false);
64 - const { user } = useContext(UserContext);
28 const router = useRouter();
66 -
67 - const buttonStyle = (
68 - votes: SongProviderVote[],
69 - type: VoteType
70 - ): CSSProperties => {
71 - if (!user) {
72 - return { cursor: "pointer" };
73 - }
74 -
75 - if (type === VoteType.UP) {
76 - if (shouldDisableButton(votes, VoteType.UP, user)) {
77 - return { pointerEvents: "none", opacity: "0.4" };
78 - } else {
79 - return { cursor: "pointer" };
80 - }
81 - } else {
82 - if (shouldDisableButton(votes, VoteType.DOWN, user)) {
83 - return { pointerEvents: "none", opacity: "0.4" };
84 - } else {
85 - return { cursor: "pointer" };
86 - }
87 - }
88 - };
89 -
90 - const calculateVotes = (votes: SongProviderVote[]): number => {
91 - if (votes.length === 0) {
92 - return 0;
93 - }
94 -
95 - const up = votes.filter((x) => x.vote_type === VoteType.UP).length;
96 - const down = votes.filter((x) => x.vote_type === VoteType.DOWN).length;
97 - const votesAggregate = up - down;
98 - return votesAggregate;
99 - };
100 -
101 - const shouldDisableButton = (
102 - votes: SongProviderVote[],
103 - type: VoteType,
104 - user: CurrentUser
105 - ) => {
106 - if (!user) {
107 - return false;
108 - }
109 - const x = votes.filter((y) => {
110 - if (y.user_id === user.user.id && y.vote_type === type) return true;
111 - return false;
112 - });
113 - return x.length > 0;
114 - };
115 -
116 - const [voteState, setVoteState] = useState<VoteState>({
117 - aggregate: calculateVotes(votes),
118 - buttonUpStyle: buttonStyle(votes, VoteType.UP),
119 - buttonDownStyle: buttonStyle(votes, VoteType.DOWN),
120 - upVotes: votes.filter((x) => x.vote_type === VoteType.UP),
121 - downVotes: votes.filter((x) => x.vote_type === VoteType.DOWN),
122 - });
123 -
124 - const vote = async (type: VoteType) => {
125 - if (!user || !user.accessToken || user.accessToken.length <= 1) {
126 - setGoToLogin(true);
127 - return;
128 - }
129 -
130 - setState(State.LOADING);
131 - try {
132 - const payload: VotePayload = {
133 - songProviderId,
134 - voteType: type,
135 - accessToken: user.accessToken,
136 - };
137 - const response = await invoke<SongProviderResponse>(TAURI_CONTENT_SONG_BRIDGE_VOTE, payload);
138 - if (response.status === TauriResponse.SUCCESS) {
139 - setVoteState({
140 - aggregate: calculateVotes(response.votes),
141 - buttonUpStyle: buttonStyle(response.votes, VoteType.UP),
142 - buttonDownStyle: buttonStyle(response.votes, VoteType.DOWN),
143 - upVotes: response.votes.filter((x) => x.vote_type === VoteType.UP),
144 - downVotes: response.votes.filter(
145 - (x) => x.vote_type === VoteType.DOWN
146 - ),
147 - });
148 - setState(State.LOADED);
149 - } else {
150 - log.error("Error voting", response);
151 - setState(State.ERROR);
152 - }
153 - } catch (error) {
154 - log.error(error);
155 - setState(State.ERROR);
156 - }
157 - }
29 + const [votesAggregate, setVotesAggregate] = useState(0);
30
31 const isDoneSplitting = audioFile && audioFile.status === Status.DONE;
32
161 - if (goToLogin) {
162 - router.push("/login");
163 - return;
164 - }
165 -
33 if (goToPlayer) {
34 + log.debug("Go to player");
35 router.push(`/play?audioFileId=${audioFile?.id}`);
36 return;
37 }
38
171 - if (state === State.LOADING) {
172 - return (
173 - <div className="flex justify-center max-h-full">
174 - <Spinner animation="border" role="status">
175 - <span className="visually-hidden">Loading...</span>
176 - </Spinner>
177 - </div>
178 - );
39 + const onVotesAggregateUpdate = (votesAggregate: number) => {
40 + setVotesAggregate(votesAggregate);
41 }
42
43 const mainView = isDoneSplitting ? (
44 <LetsPlayView onClick={() => setGoToPlayer(true)} />
45 ) : (
184 - <MainVotesView voteState={voteState} vote={vote} />
46 + <MainVotesView songProviderId={songProviderId} votes={votes} onVotesAggregateUpdate={onVotesAggregateUpdate} />
47 );
48 const mainButton = (
49 <ButtonGenerateBackingTracks
50 songProviderId={songProviderId}
51 audioFile={audioFile}
190 - aggregateVotes={voteState.aggregate}
52 + aggregateVotes={votesAggregate}
53 />
54 );
55 // Votes treshold is passed, show the generate button.