A decentralized music tracking and discovery platform built on AT Protocol 馃幍 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
at feat/pgpull 125 lines 3.4 kB view raw
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;