Teal.fm frontend powered by slices.network tealfm-slices.wisp.place
tealfm slices
at main 3.6 kB view raw
1import { graphql, useLazyLoadQuery, usePaginationFragment } from "react-relay"; 2import { useParams } from "react-router-dom"; 3import { useEffect, useMemo, useRef } from "react"; 4import type { ProfileQuery as ProfileQueryType } from "./__generated__/ProfileQuery.graphql"; 5import type { Profile_plays$key } from "./__generated__/Profile_plays.graphql"; 6import TrackItem from "./TrackItem"; 7import ProfileLayout from "./ProfileLayout"; 8 9export default function Profile() { 10 const { handle } = useParams<{ handle: string }>(); 11 12 const queryVariables = useMemo(() => { 13 return { 14 where: { actorHandle: { eq: handle } }, 15 }; 16 }, [handle]); 17 18 const queryData = useLazyLoadQuery<ProfileQueryType>( 19 graphql` 20 query ProfileQuery($where: FmTealAlphaFeedPlayWhereInput!) { 21 ...Profile_plays @arguments(where: $where) 22 } 23 `, 24 queryVariables, 25 ); 26 27 const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment< 28 ProfileQueryType, 29 Profile_plays$key 30 >( 31 graphql` 32 fragment Profile_plays on Query 33 @refetchable(queryName: "ProfilePaginationQuery") 34 @argumentDefinitions( 35 cursor: { type: "String" } 36 count: { type: "Int", defaultValue: 20 } 37 where: { type: "FmTealAlphaFeedPlayWhereInput!" } 38 ) { 39 fmTealAlphaFeedPlays( 40 first: $count 41 after: $cursor 42 sortBy: [{ field: playedTime, direction: desc }] 43 where: $where 44 ) 45 @connection( 46 key: "Profile_fmTealAlphaFeedPlays" 47 filters: ["where", "sortBy"] 48 ) { 49 totalCount 50 edges { 51 node { 52 ...TrackItem_play 53 } 54 } 55 } 56 } 57 `, 58 queryData, 59 ); 60 61 const loadMoreRef = useRef<HTMLDivElement>(null); 62 63 const plays = useMemo( 64 () => 65 data?.fmTealAlphaFeedPlays?.edges?.map((edge) => edge.node).filter((n) => 66 n != null 67 ) || [], 68 [data?.fmTealAlphaFeedPlays?.edges], 69 ); 70 71 useEffect(() => { 72 window.scrollTo(0, 0); 73 }, [handle]); 74 75 useEffect(() => { 76 if (!loadMoreRef.current || !hasNext) return; 77 78 const observer = new IntersectionObserver( 79 (entries) => { 80 if (entries[0].isIntersecting && hasNext && !isLoadingNext) { 81 loadNext(20); 82 } 83 }, 84 { threshold: 0.1 }, 85 ); 86 87 observer.observe(loadMoreRef.current); 88 89 return () => observer.disconnect(); 90 }, [hasNext, isLoadingNext, loadNext]); 91 92 return ( 93 //@ts-expect-error: idk 94 <ProfileLayout handle={handle!}> 95 <div className="mb-8"> 96 <p className="text-xs text-zinc-500 uppercase tracking-wider"> 97 {(data?.fmTealAlphaFeedPlays?.totalCount ?? 0).toLocaleString()}{" "} 98 scrobbles 99 </p> 100 </div> 101 102 <div className="space-y-1"> 103 {plays && plays.length > 0 104 ? ( 105 plays.map((play, index) => <TrackItem key={index} play={play} />) 106 ) 107 : ( 108 <p className="text-zinc-600 text-center py-8 text-xs uppercase tracking-wider"> 109 No tracks found for this user 110 </p> 111 )} 112 </div> 113 114 {hasNext && ( 115 <div ref={loadMoreRef} className="py-12 text-center"> 116 {isLoadingNext 117 ? ( 118 <p className="text-xs text-zinc-600 uppercase tracking-wider"> 119 Loading... 120 </p> 121 ) 122 : ( 123 <p className="text-xs text-zinc-700 uppercase tracking-wider"> 124 · 125 </p> 126 )} 127 </div> 128 )} 129 </ProfileLayout> 130 ); 131}