A decentralized music tracking and discovery platform built on AT Protocol 馃幍
rocksky.app
spotify
atproto
lastfm
musicbrainz
scrobbling
listenbrainz
1import { Link } from "@tanstack/react-router";
2import { Avatar } from "baseui/avatar";
3import { Block } from "baseui/block";
4import { StatefulPopover, TRIGGER_TYPE } from "baseui/popover";
5import { LabelMedium, LabelSmall } from "baseui/typography";
6import { useAtom } from "jotai";
7import { useEffect } from "react";
8import { profilesAtom } from "../../atoms/profiles";
9import { statsAtom } from "../../atoms/stats";
10import {
11 useProfileByDidQuery,
12 useProfileStatsByDidQuery,
13} from "../../hooks/useProfile";
14import Stats from "../Stats";
15import NowPlaying from "./NowPlaying";
16
17export type HandleProps = {
18 link: string;
19 did: string;
20};
21
22function Handle(props: HandleProps) {
23 const { link, did } = props;
24 const [profiles, setProfiles] = useAtom(profilesAtom);
25 const profile = useProfileByDidQuery(did);
26 const profileStats = useProfileStatsByDidQuery(did);
27 const [stats, setStats] = useAtom(statsAtom);
28
29 useEffect(() => {
30 if (profile.isLoading || profile.isError) {
31 return;
32 }
33
34 if (!profile.data || !did) {
35 return;
36 }
37
38 setProfiles((profiles) => ({
39 ...profiles,
40 [did]: {
41 avatar: profile.data.avatar,
42 displayName: profile.data.displayName,
43 handle: profile.data.handle,
44 spotifyConnected: profile.data.spotifyConnected,
45 createdAt: profile.data.createdAt,
46 did,
47 },
48 }));
49
50 // eslint-disable-next-line react-hooks/exhaustive-deps
51 }, [profile.data, profile.isLoading, profile.isError, did]);
52
53 useEffect(() => {
54 if (profileStats.isLoading || profileStats.isError) {
55 return;
56 }
57
58 if (!profileStats.data || !did) {
59 return;
60 }
61
62 setStats((prev) => ({
63 ...prev,
64 [did]: {
65 scrobbles: profileStats.data.scrobbles,
66 artists: profileStats.data.artists,
67 lovedTracks: profileStats.data.lovedTracks,
68 albums: profileStats.data.albums,
69 tracks: profileStats.data.tracks,
70 },
71 }));
72 // eslint-disable-next-line react-hooks/exhaustive-deps
73 }, [profileStats.data, profileStats.isLoading, profileStats.isError, did]);
74
75 return (
76 <StatefulPopover
77 content={() => (
78 <Block className="!bg-[var(--color-background)] !text-[var(--color-text)] p-[15px] w-[380px] rounded-[6px] border-[1px] border-[var(--color-border)]">
79 <div className="flex flex-row items-center">
80 <Link to={link} className="no-underline">
81 <Avatar
82 src={profiles[did]?.avatar}
83 name={profiles[did]?.displayName}
84 size={"60px"}
85 />
86 </Link>
87 <div className="ml-[16px]">
88 <Link to={link} className="no-underline">
89 <LabelMedium
90 marginTop={"10px"}
91 className="!text-[var(--color-text)]"
92 >
93 {profiles[did]?.displayName}
94 </LabelMedium>
95 </Link>
96 <a
97 href={`https://bsky.app/profile/${profiles[did]?.handle}`}
98 className="no-underline text-[var(--color-primary)]"
99 >
100 <LabelSmall className="!text-[var(--color-primary)] mt-[3px] mb-[25px]">
101 @{did}
102 </LabelSmall>
103 </a>
104 </div>
105 </div>
106
107 {stats[did] && <Stats stats={stats[did]} mb={1} />}
108
109 <NowPlaying did={did} />
110 </Block>
111 )}
112 triggerType={TRIGGER_TYPE.hover}
113 autoFocus={false}
114 focusLock={false}
115 >
116 <Link to={link} className="no-underline">
117 <LabelMedium className="!text-[var(--color-primary)] !overflow-hidden !text-ellipsis !max-w-[220px] !text-[14px]">
118 @{did}
119 </LabelMedium>
120 </Link>
121 </StatefulPopover>
122 );
123}
124
125export default Handle;