forked from
chadtmiller.com/slices-teal-relay
Teal.fm frontend powered by slices.network
tealfm-slices.wisp.place
tealfm
slices
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}