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