profile page
Seto Elkahfi committed
Aug 4, 2024 at 11:48 UTC
eba8d47b911f3a63e88f060c56bda0e064df80eb
10 files changed
+127
-40
frontend/splitfire-desktop/app/_ui/global-nav.tsx
+12
-1
@@ -8,6 +8,8 @@ import clsx from 'clsx';
8
import { useContext, useState } from 'react';
9
import Image from 'next/image';
10
import { UserContext } from '../_src/lib/CurrentUserContext';
11
+import { useLogger } from '@/lib/logger';
12
+import { PARAMS_USER_ID } from '../profile/page';
13
14
export function GlobalNav() {
15
@@ -94,9 +96,18 @@ function LoggedOutNav() {
96
}
97
98
function LoggedInNav() {
99
+ const log = useLogger('LoggedInNav');
100
+ const currentUser = useContext(UserContext);
101
+ const user = currentUser?.user?.user;
102
+ // Sanity check
103
+ if (!user) {
104
+ log.error('No user context');
105
+ return null;
106
+ }
107
+
108
return (
109
<div className="space-y-1">
99
- <GlobalNavItem item={{ name: 'Profile', slug: 'profile' }} close={() => {}} />
110
+ <GlobalNavItem item={{ name: 'Profile', slug: `profile?${PARAMS_USER_ID}=${user.id}` }} close={() => {}} />
111
<GlobalNavItem item={{ name: 'Logout', slug: 'logout' }} close={() => {}} />
112
</div>
113
);
frontend/splitfire-desktop/app/play/_components/audio-info-middle.tsx
+1
-1
@@ -1,5 +1,5 @@
1
-import { useLogger } from "@/app/_src/lib/logger";
1
import { useCountDown } from "@/app/_src/lib/useCountDown";
2
+import { useLogger } from "@/lib/logger";
3
import { useEffect } from "react";
4
5
export function AudioInfoMiddle({
frontend/splitfire-desktop/app/play/_components/control-button.tsx
+1
-1
@@ -1,3 +1,4 @@
1
+import { useLogger } from "@/lib/logger";
2
import {
3
BookmarkIcon,
4
PauseIcon,
@@ -7,7 +8,6 @@ import {
8
PlayIcon,
9
ResetIcon,
10
} from "@radix-ui/react-icons";
10
-import { useLogger } from "@/app/_src/lib/logger";
11
import { useState } from "react";
12
13
export function ControlButtons(props: {
frontend/splitfire-desktop/app/play/_components/player.tsx
+1
-1
@@ -2,7 +2,6 @@
2
3
import { ModeDemucs } from "@/app/_src/components/player/models/Mode";
4
import { PlayerState, PlayerVolume } from "@/app/_src/components/player/Player";
5
-import { useLogger } from "@/app/_src/lib/logger";
5
import {
6
TAURI_PLAYER_PAUSED,
7
TAURI_PLAYER_PLAY,
@@ -18,6 +17,7 @@ import { useState } from "react";
17
import { ControlButtons } from "./control-button";
18
import { AudioInfo } from "./audio-info";
19
import { AudioInfoMiddle } from "./audio-info-middle";
20
+import { useLogger } from "@/lib/logger";
21
22
export default function Player({
23
audioId,
frontend/splitfire-desktop/app/profile/page.tsx
+106
-31
@@ -1,31 +1,42 @@
1
"use client";
2
3
-import { useContext, useEffect, useState } from "react";
4
-import { UserContext } from "../_src/lib/CurrentUserContext";
3
+import { useEffect, useState } from "react";
4
import { useLogger } from "../../lib/logger";
6
-import { usernameOrId } from "../_src/models/user";
5
+import User, { usernameOrId } from "../_src/models/user";
6
import { useSearchParams } from "next/navigation";
7
import Image from "next/image";
8
import { invoke } from "@tauri-apps/api";
9
import { TAURI_ACCOUNT_PROFILE } from "../_src/lib/tauriHandler";
10
import { AccountProfileResponse } from "@/models/account";
11
import { TauriResponse } from "@/models/shared";
13
-import { CheckCircleIcon } from "@heroicons/react/outline";
12
+import { LocationMarkerIcon } from "@heroicons/react/solid";
13
+import { GearIcon } from "@radix-ui/react-icons";
14
+import Link from "next/link";
15
15
-export default function Page() {
16
+export const PARAMS_USER_ID = "userId";
17
+
18
+enum State {
19
+ LOADING,
20
+ LOADED,
21
+ ERROR,
22
+}
23
+
24
+export default function Page({ loggedInUser }: { loggedInUser: User | null }) {
25
const log = useLogger("Profile/Page");
26
const searchParams = useSearchParams();
18
- const userId = searchParams.get("userId");
19
- const currentUser = useContext(UserContext);
27
+ const userId = searchParams.get(PARAMS_USER_ID);
28
const [isOwnProfile, setIsOwnProfile] = useState(false);
21
- log.debug("currentUser");
22
- const user = currentUser?.user?.user;
29
+ const [userDisplayed, setUserDisplayed] = useState<User | null>(null);
30
+ const [state, setState] = useState(State.LOADING);
31
32
useEffect(() => {
33
log.debug("Profile page loaded.");
34
async function fetchData() {
35
try {
28
- const result = await invoke<AccountProfileResponse>(TAURI_ACCOUNT_PROFILE, { userId });
36
+ const result = await invoke<AccountProfileResponse>(
37
+ TAURI_ACCOUNT_PROFILE,
38
+ { userId }
39
+ );
40
log.debug("ProfileResponse", result);
41
if (result.status === TauriResponse.ERROR) {
42
log.error("Error fetching profile");
@@ -33,41 +44,105 @@ export default function Page() {
44
}
45
if (!result.user) {
46
log.error("No user found");
47
+ setState(State.ERROR);
48
return;
49
}
50
39
- setIsOwnProfile(result.user.id === currentUser?.user?.user.id);
51
+ setIsOwnProfile(result.user.id === loggedInUser?.id);
52
+ setUserDisplayed(result.user);
53
+ setState(State.LOADED);
54
} catch (error) {
55
log.error(error);
56
+ setState(State.ERROR);
57
}
58
}
59
fetchData();
60
// eslint-disable-next-line react-hooks/exhaustive-deps
61
}, []);
47
- // Sanity check
48
- if (!user) {
49
- log.error("No user context");
50
- return <div>No user context</div>;
62
+
63
+ if (state === State.LOADING) {
64
+ return (
65
+ <div className="prose prose-sm prose-invert max-w-none grid-rows-3 gap-6">
66
+ Loading...
67
+ </div>
68
+ );
69
+ }
70
+
71
+ if (state === State.ERROR) {
72
+ return (
73
+ <div className="prose prose-sm prose-invert max-w-none grid-rows-3 gap-6">
74
+ Error loading profile
75
+ </div>
76
+ );
77
+ }
78
+
79
+ if (!userDisplayed) {
80
+ return (
81
+ <div className="prose prose-sm prose-invert max-w-none grid-rows-3 gap-6">
82
+ User not found
83
+ </div>
84
+ );
85
}
86
87
return (
88
<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">
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>
89
+ <div className="w-full px-4 mx-auto">
90
+ <div className="relative flex flex-col min-w-0 break-words mb-6 shadow-xl rounded-lg mt-16">
91
+ <div className="px-6">
92
+ <div className="flex flex-wrap justify-center">
93
+ <div className="w-full px-4 flex justify-center">
94
+ <div className="">
95
+ <Image
96
+ src={userDisplayed.gravatar_url}
97
+ width={150}
98
+ height={150}
99
+ className="shadow-xl rounded-full h-auto align-middle border-none absolute -m-16 -ml-20 max-w-150-px"
100
+ alt={userDisplayed.name}
101
+ />
102
+ </div>
103
+ </div>
104
+ <div className="w-full px-4 text-center mt-20">
105
+ <div className="flex justify-center py-4 lg:pt-4 pt-8">
106
+ <div className="mr-4 p-3 text-center">
107
+ <span className="text-xl font-bold block uppercase tracking-wide text-blueGray-600">
108
+ {userDisplayed.followers_count}
109
+ </span>
110
+ <span className="text-sm text-blueGray-400">Followers</span>
111
+ </div>
112
+ <div className="lg:mr-4 p-3 text-center">
113
+ <span className="text-xl font-bold block uppercase tracking-wide text-blueGray-600">
114
+ {userDisplayed.following_count}
115
+ </span>
116
+ <span className="text-sm text-blueGray-400">Following</span>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ <div className="text-center mt-12">
122
+ <h3 className="text-xl font-semibold leading-normal text-blueGray-700 mb-2">
123
+ {userDisplayed.name} <span className="text-blueGray-400 font-normal">(@{usernameOrId(userDisplayed)})</span>
124
+ </h3>
125
+ {isOwnProfile && (
126
+ <div className="absolute -m-4 -mr-4">
127
+ <Link href="/profile/update">
128
+ <GearIcon className="h-6 w-6 text-blueGray-300" />
129
+ </Link>
130
+ </div>
131
+ )}
132
+ <div className="text-sm leading-normal mt-0 mb-2 text-blueGray-400 font-bold uppercase">
133
+ <LocationMarkerIcon className="h-4 w-4 inline-block" /> Stockholm
134
+ </div>
135
+ </div>
136
+ <div className="mt-10 py-10 border-t border-blueGray-200 text-center">
137
+ <div className="flex flex-wrap justify-center">
138
+ <div className="w-full lg:w-9/12 px-4">
139
+ <p className="mb-4 text-lg leading-relaxed text-blueGray-700">
140
+ {userDisplayed.about}
141
+ </p>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </div>
146
</div>
147
</div>
148
</div>
frontend/splitfire-desktop/app/split/_components/voters-view.tsx
+2
-1
@@ -1,5 +1,6 @@
1
import { VoteType } from "@/app/_src/components/pages/song/components/UpDownVotes";
2
import { SongProviderVote } from "@/app/_src/models/SongVotesDetailResponse";
3
+import { PARAMS_USER_ID } from "@/app/profile/page";
4
import Image from "next/image";
5
import Link from "next/link";
6
@@ -14,7 +15,7 @@ export function VoterGravatarsViews(props: {
15
<div className="flex -space-x-2 overflow-hidden">
16
{props.voters.map((x, i) => {
17
return (
17
- <Link href={`/profile?userId=@${x.user_id}`} key={i}>
18
+ <Link href={`/profile?${PARAMS_USER_ID}=@${x.user_id}`} key={i}>
19
<Image
20
src={x.voter_gravatar}
21
width={24}
frontend/splitfire-desktop/app/split/_components/votes-view-main.tsx
+1
-1
@@ -6,11 +6,11 @@ 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";
9
import { UserContext } from "@/app/_src/lib/CurrentUserContext";
10
import { useRouter } from "next/navigation";
11
import { CurrentUser } from "@/app/_src/lib/db";
12
import { Spinner } from "react-bootstrap";
13
+import { useLogger } from "@/lib/logger";
14
15
export enum VoteType {
16
UP = "up",
frontend/splitfire-desktop/app/split/_components/votes-view.tsx
+1
-1
@@ -10,7 +10,7 @@ import { useState } from "react";
10
import ButtonGenerateBackingTracks from "./button-split";
11
import MainVotesView, { } from "./votes-view-main";
12
import LetsPlayView from "./lets-play";
13
-import { useLogger } from "@/app/_src/lib/logger";
13
+import { useLogger } from "@/lib/logger";
14
15
export default function VotesView({
16
songProviderId,
frontend/splitfire-desktop/lib/logger.ts
+1
-1
@@ -3,7 +3,7 @@ import { Logger } from "tslog"
3
export const useLogger = (name: string) => {
4
return new Logger({
5
name: name,
6
- type: 'pretty',
6
+ type: 'json',
7
minLevel: process.env.NODE_ENV === "production" ? 6: 0,
8
})
9
}
\ No newline at end of file
frontend/splitfire-desktop/src-tauri/src/rest/account.rs
+1
-1
@@ -168,7 +168,7 @@ pub async fn account_profile(user_id: String) -> AccountProfileResponse {
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))
171
+ .get(&url)
172
.send()
173
.await;
174