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, Link } from "react-router-dom";
3import { useEffect, 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";
7
8export default function Profile() {
9 const { handle } = useParams<{ handle: string }>();
10
11 const queryData = useLazyLoadQuery<ProfileQueryType>(
12 graphql`
13 query ProfileQuery($where: FmTealAlphaFeedPlayWhereInput!) {
14 ...Profile_plays @arguments(where: $where)
15 }
16 `,
17 {
18 where: { actorHandle: { eq: handle } },
19 }
20 );
21
22 const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment<
23 ProfileQueryType,
24 Profile_plays$key
25 >(
26 graphql`
27 fragment Profile_plays on Query
28 @refetchable(queryName: "ProfilePaginationQuery")
29 @argumentDefinitions(
30 cursor: { type: "String" }
31 count: { type: "Int", defaultValue: 20 }
32 where: { type: "FmTealAlphaFeedPlayWhereInput!" }
33 ) {
34 fmTealAlphaFeedPlays(
35 first: $count
36 after: $cursor
37 sortBy: [{ field: "playedTime", direction: desc }]
38 where: $where
39 )
40 @connection(
41 key: "Profile_fmTealAlphaFeedPlays"
42 filters: ["where", "sortBy"]
43 ) {
44 totalCount
45 edges {
46 node {
47 ...TrackItem_play
48 actorHandle
49 appBskyActorProfile {
50 displayName
51 description
52 avatar {
53 url(preset: "avatar")
54 }
55 }
56 }
57 }
58 }
59 }
60 `,
61 queryData
62 );
63
64 const loadMoreRef = useRef<HTMLDivElement>(null);
65
66 const plays = data?.fmTealAlphaFeedPlays?.edges?.map((edge) => edge.node).filter((n) => n != null) || [];
67 const profile = plays?.[0]?.appBskyActorProfile;
68
69 useEffect(() => {
70 window.scrollTo(0, 0);
71 }, [handle]);
72
73 useEffect(() => {
74 if (!loadMoreRef.current || !hasNext) return;
75
76 const observer = new IntersectionObserver(
77 (entries) => {
78 if (entries[0].isIntersecting && hasNext && !isLoadingNext) {
79 loadNext(20);
80 }
81 },
82 { threshold: 0.1 }
83 );
84
85 observer.observe(loadMoreRef.current);
86
87 return () => observer.disconnect();
88 }, [hasNext, isLoadingNext, loadNext]);
89
90 return (
91 <div className="min-h-screen bg-zinc-950 text-zinc-300 font-mono">
92 <div className="max-w-4xl mx-auto px-6 py-12">
93 <Link
94 to="/"
95 className="px-2 py-1 text-xs text-zinc-500 hover:text-zinc-300 transition-colors inline-block mb-8"
96 >
97 ← Back
98 </Link>
99
100 <div className="mb-12 flex items-start gap-6 border-b border-zinc-800 pb-6">
101 {profile?.avatar?.url && (
102 <img
103 src={profile.avatar.url}
104 alt={profile.displayName ?? handle ?? "User"}
105 className="w-16 h-16 flex-shrink-0 object-cover"
106 />
107 )}
108 <div className="flex-1">
109 <h1 className="text-lg font-medium mb-1 text-zinc-100">
110 {profile?.displayName ?? handle}
111 </h1>
112 <p className="text-xs text-zinc-500 mb-2">@{handle}</p>
113 {profile?.description && (
114 <p className="text-xs text-zinc-400">{profile.description}</p>
115 )}
116 </div>
117 </div>
118
119 <div className="mb-8">
120 <h2 className="text-sm font-medium uppercase tracking-wider text-zinc-400 mb-2">Recent Tracks</h2>
121 <p className="text-xs text-zinc-500 uppercase tracking-wider">
122 {(data?.fmTealAlphaFeedPlays?.totalCount ?? 0).toLocaleString()} scrobbles
123 </p>
124 </div>
125
126 <div className="space-y-1">
127 {plays && plays.length > 0 ? (
128 plays.map((play, index) => <TrackItem key={index} play={play} />)
129 ) : (
130 <p className="text-zinc-600 text-center py-8 text-xs uppercase tracking-wider">
131 No tracks found for this user
132 </p>
133 )}
134 </div>
135
136 {hasNext && (
137 <div ref={loadMoreRef} className="py-12 text-center">
138 {isLoadingNext ? (
139 <p className="text-xs text-zinc-600 uppercase tracking-wider">Loading...</p>
140 ) : (
141 <p className="text-xs text-zinc-700 uppercase tracking-wider">·</p>
142 )}
143 </div>
144 )}
145 </div>
146 </div>
147 );
148}