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.