wip
Seto Elkahfi committed
Aug 2, 2024 at 18:26 UTC
9ccfbdfe8170496c4f4d2741c4c0d48d26a434a1
15 files changed
+214
-84
backend/musik88-web/.rubocop.yml
+3
-1
@@ -1,4 +1,6 @@
1
-Documentation:
1
+AllCops:
2
+ NewCops: disable
3
+Style/Documentation:
4
Enabled: false
5
Style/HashSyntax:
6
Enabled: false
\ No newline at end of file
backend/musik88-web/app/models/user.rb
+1
-1
@@ -18,7 +18,7 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength
18
19
before_save { email.downcase! }
20
21
- VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
21
+ VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d-]+(\.[a-z\d-]+)*\.[a-z]+\z/i
22
validates :email, presence: true,
23
length: { maximum: 255 },
24
format: { with: VALID_EMAIL_REGEX },
frontend/splitfire-desktop/app/_src/lib/tauriHandler.ts
+2
@@ -16,12 +16,14 @@ export const TAURI_PLAYER_RECORDING_LENGTH = 'player_recording_length' // in se
16
export const TAURI_ACCOUNT_LOGIN = 'account_login'
17
export const TAURI_ACCOUNT_LOGOUT = 'account_logout'
18
export const TAURI_ACCOUNT_REGISTER = 'account_register'
19
+export const TAURI_ACCOUNT_PROFILE = 'account_profile'
20
21
// Contents
22
export const TAURI_CONTENT_CAROUSEL = 'content_carousel'
23
export const TAURI_CONTENT_READY_TO_PLAY = 'content_ready_to_play'
24
export const TAURI_CONTENT_SONG_BRIDGE_DETAIL = 'content_song_bridge_detail'
25
export const TAURI_CONTENT_TOP_VOTED = 'content_top_voted'
26
+export const TAURI_CONTENT_SONG_BRIDGE_VOTE = 'content_song_bridge_vote'
27
28
// Settings
29
export const TAURI_SET_ENVIRONMENT = 'set_environment'
frontend/splitfire-desktop/app/login/page.tsx
+3
-3
@@ -72,7 +72,7 @@ export default function Page() {
72
event.preventDefault();
73
}}
74
>
75
- <Form.Field className="FormField" name="email">
75
+ <Form.Field className="FormField my-6" name="email">
76
<div
77
style={{
78
display: "flex",
@@ -98,7 +98,7 @@ export default function Page() {
98
/>
99
</Form.Control>
100
</Form.Field>
101
- <Form.Field className="FormField" name="password">
101
+ <Form.Field className="FormField my-6" name="password">
102
<div
103
style={{
104
display: "flex",
@@ -126,7 +126,7 @@ export default function Page() {
126
/>
127
</Form.Control>
128
</Form.Field>
129
- <Form.Field className="FormField" name="password">
129
+ <Form.Field className="FormField my-6" name="password">
130
<Button onClick={() => login(update)} variant={"outline"} size={"lg"}>
131
Login
132
</Button>
frontend/splitfire-desktop/app/profile/page.tsx
+41
-14
@@ -1,21 +1,49 @@
1
"use client";
2
3
-import { useContext } from "react";
3
+import { useContext, useEffect, useState } from "react";
4
import { UserContext } from "../_src/lib/CurrentUserContext";
5
import { useLogger } from "../_src/lib/logger";
6
-import { SkeletonCard } from "../_ui/skeleton-card";
6
import { usernameOrId } from "../_src/models/user";
7
import { useSearchParams } from "next/navigation";
8
import Image from "next/image";
9
+import { invoke } from "@tauri-apps/api";
10
+import { TAURI_ACCOUNT_PROFILE } from "../_src/lib/tauriHandler";
11
+import { AccountProfileResponse } from "@/models/account";
12
+import { TauriResponse } from "@/models/shared";
13
+import { CheckCircleIcon } from "@heroicons/react/outline";
14
15
export default function Page() {
16
const log = useLogger("Profile/Page");
17
const searchParams = useSearchParams();
18
const userId = searchParams.get("userId");
19
const currentUser = useContext(UserContext);
20
+ const [isOwnProfile, setIsOwnProfile] = useState(false);
21
log.debug("currentUser");
17
-
22
const user = currentUser?.user?.user;
23
+
24
+ useEffect(() => {
25
+ log.debug("Profile page loaded.");
26
+ async function fetchData() {
27
+ try {
28
+ const result = await invoke<AccountProfileResponse>(TAURI_ACCOUNT_PROFILE, { userId });
29
+ log.debug("ProfileResponse", result);
30
+ if (result.status === TauriResponse.ERROR) {
31
+ log.error("Error fetching profile");
32
+ return;
33
+ }
34
+ if (!result.user) {
35
+ log.error("No user found");
36
+ return;
37
+ }
38
+
39
+ setIsOwnProfile(result.user.id === currentUser?.user?.user.id);
40
+ } catch (error) {
41
+ log.error(error);
42
+ }
43
+ }
44
+ fetchData();
45
+ // eslint-disable-next-line react-hooks/exhaustive-deps
46
+ }, []);
47
// Sanity check
48
if (!user) {
49
log.error("No user context");
@@ -23,25 +51,24 @@ export default function Page() {
51
}
52
53
return (
26
- <div className="prose prose-sm prose-invert max-w-none">
27
- <div className="flex justify-between">
54
+ <div className="prose prose-sm prose-invert max-w-none grid-rows-3 gap-6">
55
+ <div className="flex justify-between">
56
<div className="self-start">
57
<h1 className="text-3xl font-bold">{user.name}</h1>
58
+ {(isOwnProfile) && (<CheckCircleIcon className="h-6 w-6 text-green-500" />)}
59
</div>
60
</div>
61
<div className="grid grid-rows-2 grid-flow-row auto-rows-max">
62
<div className="max-h-1">
34
- <Image src={user.gravatar_url} alt="avatar" width={50} height={50} className="rounded-full"/>
63
+ <Image
64
+ src={user.gravatar_url}
65
+ alt="avatar"
66
+ width={50}
67
+ height={50}
68
+ className="rounded-full"
69
+ />
70
<h2>@{usernameOrId(user)}</h2>
71
</div>
37
- <div className="max-w-none">
38
- <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
39
- <h2 className="text-2xl font-bold">Your public plays</h2>
40
- {Array.from({ length: 6 }).map((_, i) => (
41
- <SkeletonCard key={i} />
42
- ))}
43
- </div>
44
- </div>
72
</div>
73
</div>
74
);
frontend/splitfire-desktop/app/register/page.tsx
+5
-4
@@ -3,10 +3,11 @@
3
import { invoke } from "@tauri-apps/api";
4
import { useState } from "react";
5
import * as Form from "@radix-ui/react-form";
6
-import { TAURI_ACCOUNT_REGISTER, TauriResponse } from "../_src/lib/tauriHandler";
6
+import { TAURI_ACCOUNT_REGISTER } from "../_src/lib/tauriHandler";
7
import { Button } from "../_ui/components/button";
8
import { useLogger } from "../_src/lib/logger";
9
import { AccountRegisterResponse } from "@/models/account";
10
+import { TauriResponse } from "@/models/shared";
11
12
enum State {
13
LOADING,
@@ -99,7 +100,7 @@ function SignupForm({ registerCallback }: { registerCallback: (state: SignupStat
100
event.preventDefault();
101
}}
102
>
102
- <Form.Field className="FormField" name="name">
103
+ <Form.Field className="FormField my-6" name="name">
104
<div
105
style={{
106
display: "flex",
@@ -151,7 +152,7 @@ function SignupForm({ registerCallback }: { registerCallback: (state: SignupStat
152
/>
153
</Form.Control>
154
</Form.Field>
154
- <Form.Field className="FormField" name="password">
155
+ <Form.Field className="FormField my-6" name="password">
156
<div
157
style={{
158
display: "flex",
@@ -179,7 +180,7 @@ function SignupForm({ registerCallback }: { registerCallback: (state: SignupStat
180
/>
181
</Form.Control>
182
</Form.Field>
182
- <Form.Field className="FormField" name="button">
183
+ <Form.Field className="FormField my-6" name="button">
184
<Button onClick={register} variant={"outline"} size={"lg"}>
185
{buttonText}
186
</Button>
frontend/splitfire-desktop/app/split/_components/song-votes.tsx
+1
-1
@@ -17,7 +17,7 @@ export default function SongVotes({
17
<h1 className="my-2">{songProvider.name}</h1>
18
<UpDownVotesView
19
votes={votes}
20
- providerId={songProvider.id}
20
+ songProviderId={songProvider.id}
21
audioFile={songProvider.audio_file}
22
/>
23
<Image
frontend/splitfire-desktop/app/split/_components/voters-view.tsx
+4
-4
@@ -8,11 +8,10 @@ export function VoterGravatarsViews(props: {
8
type: VoteType;
9
}) {
10
const className =
11
- props.type === VoteType.DOWN
12
- ? "justify-end"
13
- : "justify-start";
11
+ props.type === VoteType.DOWN ? "justify-end" : "justify-start";
12
return (
13
<div className={className}>
14
+ <div className="flex -space-x-2 overflow-hidden">
15
{props.voters.map((x, i) => {
16
return (
17
<Link href={`/profile?userId=@${x.user_id}`} key={i}>
@@ -20,12 +19,13 @@ export function VoterGravatarsViews(props: {
19
src={x.voter_gravatar}
20
width={24}
21
height={24}
23
- className="rounded-full"
22
+ className="inline-block h-8 w-8 rounded-full ring-2 ring-white"
23
alt={x.voter_username_or_id}
24
/>
25
</Link>
26
);
27
})}
28
</div>
29
+ </div>
30
);
31
}
frontend/splitfire-desktop/app/split/_components/votes-view.tsx
+38
-44
@@ -11,13 +11,15 @@ import {
11
import { UserContext } from "@/app/_src/lib/CurrentUserContext";
12
import { CurrentUser } from "@/app/_src/lib/db";
13
import { SongProviderVote } from "@/app/_src/models/SongVotesDetailResponse";
14
-import axios from "axios";
14
import { useRouter } from "next/navigation";
15
import { CSSProperties, useState, useContext } from "react";
16
import { Spinner } from "react-bootstrap";
17
import ButtonGenerateBackingTracks from "./button-split";
18
import MainVotesView from "./votes-view-main";
19
import LetsPlayView from "./lets-play";
20
+import { invoke } from "@tauri-apps/api";
21
+import { TAURI_CONTENT_SONG_BRIDGE_VOTE } from "@/app/_src/lib/tauriHandler";
22
+import { useLogger } from "@/app/_src/lib/logger";
23
24
enum State {
25
LOADING,
@@ -39,14 +41,15 @@ export interface VoteState {
41
}
42
43
export default function UpDownVotesView({
42
- providerId,
44
+ songProviderId,
45
votes,
46
audioFile,
47
}: {
46
- providerId: string;
48
+ songProviderId: string;
49
votes: SongProviderVote[];
50
audioFile: AudioFile | null;
51
}) {
52
+ const log = useLogger("Votes view");
53
const [state, setState] = useState(State.LOADED);
54
const [goToLogin, setGoToLogin] = useState(false);
55
const [goToPlayer, setGoToPlayer] = useState(false);
@@ -83,8 +86,6 @@ export default function UpDownVotesView({
86
87
const up = votes.filter((x) => x.vote_type === VoteType.UP).length;
88
const down = votes.filter((x) => x.vote_type === VoteType.DOWN).length;
86
- //console.log('upvotes', up)
87
- //console.log('downvotes', down)
89
const votesAggregate = up - down;
90
return votesAggregate;
91
};
@@ -112,48 +113,41 @@ export default function UpDownVotesView({
113
downVotes: votes.filter((x) => x.vote_type === VoteType.DOWN),
114
});
115
115
- const vote = (type: VoteType) => {
116
+ const vote = async (type: VoteType) => {
117
if (!user || !user.accessToken || user.accessToken.length <= 1) {
118
setGoToLogin(true);
119
return;
120
}
121
122
setState(State.LOADING);
122
- axios
123
- .post(
124
- `/song-bridge/${providerId}/vote`,
125
- { vote_type: type, provider_id: providerId },
126
- {
127
- headers: {
128
- "Content-Type": "application/json",
129
- Authorization: user.accessToken,
130
- },
131
- }
132
- )
133
- .then((res) => {
134
- const response: SongBridgeResponse = res.data;
135
- //console.log(response.votes)
136
- if (response.code === HTTPStatusCode.OK) {
137
- setVoteState({
138
- aggregate: calculateVotes(response.votes),
139
- buttonUpStyle: buttonStyle(response.votes, VoteType.UP),
140
- buttonDownStyle: buttonStyle(response.votes, VoteType.DOWN),
141
- upVotes: response.votes.filter((x) => x.vote_type === VoteType.UP),
142
- downVotes: response.votes.filter(
143
- (x) => x.vote_type === VoteType.DOWN
144
- ),
145
- });
146
- setState(State.LOADED);
147
- } else {
148
- console.log("error");
149
- setState(State.ERROR);
150
- }
151
- })
152
- .catch((error) => {
153
- console.log(error);
123
+ try {
124
+ const payload = {
125
+ songProviderId,
126
+ type,
127
+ accessToken: user.accessToken,
128
+ };
129
+ const response = await invoke<SongBridgeResponse>(TAURI_CONTENT_SONG_BRIDGE_VOTE, { payload });
130
+ log.debug(response.votes);
131
+ if (response.code === HTTPStatusCode.OK) {
132
+ setVoteState({
133
+ aggregate: calculateVotes(response.votes),
134
+ buttonUpStyle: buttonStyle(response.votes, VoteType.UP),
135
+ buttonDownStyle: buttonStyle(response.votes, VoteType.DOWN),
136
+ upVotes: response.votes.filter((x) => x.vote_type === VoteType.UP),
137
+ downVotes: response.votes.filter(
138
+ (x) => x.vote_type === VoteType.DOWN
139
+ ),
140
+ });
141
+ setState(State.LOADED);
142
+ } else {
143
+ console.log("error");
144
setState(State.ERROR);
155
- });
156
- };
145
+ }
146
+ } catch (error) {
147
+ console.log(error);
148
+ setState(State.ERROR);
149
+ }
150
+ }
151
152
const isDoneSplitting = audioFile && audioFile.status === Status.DONE;
153
@@ -184,10 +178,10 @@ export default function UpDownVotesView({
178
);
179
const mainButton = (
180
<ButtonGenerateBackingTracks
187
- providerId={providerId}
188
- audioFile={audioFile}
189
- aggregateVotes={voteState.aggregate}
190
- />
181
+ providerId={songProviderId}
182
+ audioFile={audioFile}
183
+ aggregateVotes={voteState.aggregate}
184
+ />
185
);
186
// Votes treshold is passed, show the generate button.
187
return (
frontend/splitfire-desktop/models/account.ts
+7
-1
@@ -12,4 +12,10 @@ export interface AccountRegisterResponse {
12
status: TauriResponse,
13
message: string,
14
user: User | null
15
-}
\ No newline at end of file
15
+}
16
+
17
+export interface AccountProfileResponse {
18
+ status: TauriResponse,
19
+ message: string,
20
+ user: User | null
21
+}
frontend/splitfire-desktop/src-tauri/src/command/constants.rs
+1
@@ -27,6 +27,7 @@ pub const PATH_READY_TO_PLAY: &str = "api/v1/ready-to-play";
27
pub const PATH_SEARCH: &str = "api/v1//search";
28
pub const PATH_TOP_VOTES: &str = "api/v1/top-votes";
29
pub const PATH_SONG_BRIDGE_DETAIL: &str = "api/v1/song-bridge/{providerId}/detail";
30
+pub const PATH_SONG_BRIDGE_VOTE: &str = "api/v1/song-bridge/{providerId}/vote";
31
32
pub fn base_url_builder() -> URLBuilder {
33
let mut url_builder = URLBuilder::new();
frontend/splitfire-desktop/src-tauri/src/main.rs
+5
-6
@@ -22,13 +22,10 @@ use app::{
22
},
23
rest::{
24
account::{
25
- __cmd__account_login, __cmd__account_logout, __cmd__account_register, account_login,
26
- account_logout, account_register,
25
+ __cmd__account_login, __cmd__account_logout, __cmd__account_profile, __cmd__account_register, account_login, account_logout, account_profile, account_register
26
},
27
content::{
29
- __cmd__content_carousel, __cmd__content_ready_to_play,
30
- __cmd__content_song_bridge_detail, __cmd__content_top_voted, content_carousel,
31
- content_ready_to_play, content_song_bridge_detail, content_top_voted,
28
+ __cmd__content_carousel, __cmd__content_ready_to_play, __cmd__content_song_bridge_detail, __cmd__content_song_bridge_vote, __cmd__content_top_voted, content_carousel, content_ready_to_play, content_song_bridge_detail, content_top_voted, content_song_bridge_vote
29
},
30
},
31
sfai_home_dir_path,
@@ -69,6 +66,7 @@ fn main() {
66
account_login,
67
account_logout,
68
account_register,
69
+ account_profile,
70
open_lyrics_editor,
71
open_player,
72
player_record,
@@ -83,7 +81,8 @@ fn main() {
81
content_carousel,
82
content_ready_to_play,
83
content_song_bridge_detail,
86
- content_top_voted
84
+ content_top_voted,
85
+ content_song_bridge_vote
86
])
87
.run(tauri::generate_context!())
88
.expect("Error while running tauri application.");
frontend/splitfire-desktop/src-tauri/src/models/account.rs
-1
@@ -51,7 +51,6 @@ pub struct RegisterResponse {
51
pub struct AccountProfileResponse {
52
pub status: TauriResponse,
53
pub message: String,
54
- pub access_token: Option<String>,
54
pub user: Option<User>,
55
}
56
frontend/splitfire-desktop/src-tauri/src/rest/account.rs
+42
-2
@@ -1,9 +1,9 @@
1
use crate::{
2
command::constants::{
3
- base_url_builder, PATH_ACCOUNT_LOGIN, PATH_ACCOUNT_LOGOUT, PATH_ACCOUNT_REGISTER,
3
+ base_url_builder, PATH_ACCOUNT_LOGIN, PATH_ACCOUNT_LOGOUT, PATH_ACCOUNT_PROFILE, PATH_ACCOUNT_REGISTER
4
},
5
models::{
6
- account::{AccountLoginResponse, AccountRegisterResponse, LoginResponse, RegisterResponse},
6
+ account::{AccountLoginResponse, AccountProfileResponse, AccountRegisterResponse, LoginResponse, ProfileResponse, RegisterResponse},
7
player::TauriResponse,
8
},
9
};
@@ -162,6 +162,46 @@ pub async fn account_register(
162
}
163
}
164
165
+#[tauri::command]
166
+pub async fn account_profile(user_id: String) -> AccountProfileResponse {
167
+ debug!("Getting profile for user_id {}", user_id);
168
+ // Get profile
169
+ let url = account_url_builder(PATH_ACCOUNT_PROFILE).replace("{userId}", &user_id);
170
+ let response = Client::new()
171
+ .get(account_url_builder(&url))
172
+ .send()
173
+ .await;
174
+
175
+ let response = match response {
176
+ Ok(response) => response,
177
+ Err(e) => {
178
+ debug!("Failed to get response: {:?}", e);
179
+ return AccountProfileResponse {
180
+ status: TauriResponse::Error,
181
+ message: e.to_string(),
182
+ user: None,
183
+ };
184
+ }
185
+ };
186
+ let res: ProfileResponse = match response.json().await {
187
+ Ok(json) => json,
188
+ Err(e) => {
189
+ debug!("Failed to parse response: {:?}", e);
190
+ return AccountProfileResponse {
191
+ status: TauriResponse::Error,
192
+ message: e.to_string(),
193
+ user: None,
194
+ };
195
+ }
196
+ };
197
+ debug!("Profile response: {:?}", res);
198
+ AccountProfileResponse {
199
+ status: TauriResponse::Success,
200
+ message: res.message,
201
+ user: res.user,
202
+ }
203
+}
204
+
205
fn account_url_builder(path: &str) -> String {
206
let mut base_url = base_url_builder();
207
base_url.add_route(path);
frontend/splitfire-desktop/src-tauri/src/rest/content.rs
+61
-2
@@ -1,17 +1,19 @@
1
use crate::{
2
command::constants::{
3
- base_url_builder, PATH_CAROUSEL, PATH_READY_TO_PLAY, PATH_SONG_BRIDGE_DETAIL, PATH_TOP_VOTES,
3
+ base_url_builder, PATH_CAROUSEL, PATH_READY_TO_PLAY, PATH_SONG_BRIDGE_DETAIL,
4
+ PATH_SONG_BRIDGE_VOTE, PATH_TOP_VOTES,
5
},
6
models::{
7
content::{
8
CarouselResponse, ContentCarouselResponse, ContentSongBridgeResponse,
8
- SongBridgeResponse,
9
+ SongBridgeResponse, VoteType,
10
},
11
player::TauriResponse,
12
},
13
};
14
use log::{debug, error};
15
use reqwest::Client;
16
+use serde_json::json;
17
18
#[tauri::command]
19
pub async fn content_carousel() -> ContentCarouselResponse {
@@ -173,6 +175,63 @@ pub async fn content_top_voted() -> ContentCarouselResponse {
175
}
176
}
177
178
+#[tauri::command]
179
+pub async fn content_song_bridge_vote(
180
+ song_provider_id: String,
181
+ vote_type: VoteType,
182
+ access_token: String,
183
+) -> ContentSongBridgeResponse {
184
+ debug!("Song provider id: {:?}", song_provider_id);
185
+ let url = content_url_builder(PATH_SONG_BRIDGE_VOTE).replace("{providerId}", &song_provider_id);
186
+ let body = json!({ "vote_type": vote_type, "provider_id": song_provider_id }).to_string();
187
+ let response: Result<reqwest::Response, reqwest::Error> = Client::new()
188
+ .post(url)
189
+ .body(body)
190
+ .header("Authorization", format!("Bearer {}", access_token))
191
+ .send()
192
+ .await;
193
+
194
+ let response = match response {
195
+ Ok(response) => response,
196
+ Err(e) => {
197
+ error!("Failed to get response: {:?}", e);
198
+ return ContentSongBridgeResponse {
199
+ code: TauriResponse::Error,
200
+ message: e.to_string(),
201
+ song_provider: None,
202
+ votes: vec![],
203
+ };
204
+ }
205
+ };
206
+
207
+ let res: SongBridgeResponse = match response.json().await {
208
+ Ok(json) => json,
209
+ Err(e) => {
210
+ error!("Failed to parse response: {:?}", e);
211
+ return ContentSongBridgeResponse {
212
+ code: TauriResponse::Error,
213
+ message: e.to_string(),
214
+ song_provider: None,
215
+ votes: vec![],
216
+ };
217
+ }
218
+ };
219
+
220
+ debug!("Song bridge detail: {:?}", res);
221
+
222
+ let votes = match res.votes {
223
+ Some(votes) => votes,
224
+ None => vec![],
225
+ };
226
+
227
+ ContentSongBridgeResponse {
228
+ code: TauriResponse::Success,
229
+ message: res.message,
230
+ song_provider: res.song_provider,
231
+ votes,
232
+ }
233
+}
234
+
235
fn content_url_builder(path: &str) -> String {
236
let mut url_builder = base_url_builder();
237
url_builder.add_route(path);