forked from
chadtmiller.com/slices-teal-relay
Teal.fm frontend powered by slices.network
tealfm-slices.wisp.place
tealfm
slices
1import {
2 graphql,
3 useLazyLoadQuery,
4 usePaginationFragment,
5 useSubscription,
6} from "react-relay";
7import { useEffect, useRef } from "react";
8import type { AppQuery } from "./__generated__/AppQuery.graphql";
9import type { App_plays$key } from "./__generated__/App_plays.graphql";
10import type { AppSubscription } from "./__generated__/AppSubscription.graphql";
11import TrackItem from "./TrackItem";
12import Layout from "./Layout";
13import {
14 ConnectionHandler,
15 type GraphQLSubscriptionConfig,
16} from "relay-runtime";
17
18export default function App() {
19 const queryData = useLazyLoadQuery<AppQuery>(
20 graphql`
21 query AppQuery {
22 ...App_plays
23 }
24 `,
25 {}
26 );
27
28 const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment<
29 AppQuery,
30 App_plays$key
31 >(
32 graphql`
33 fragment App_plays on Query
34 @refetchable(queryName: "AppPaginationQuery")
35 @argumentDefinitions(
36 cursor: { type: "String" }
37 count: { type: "Int", defaultValue: 20 }
38 ) {
39 fmTealAlphaFeedPlays(
40 first: $count
41 after: $cursor
42 sortBy: [{ field: "playedTime", direction: desc }]
43 ) @connection(key: "App_fmTealAlphaFeedPlays", filters: ["sortBy"]) {
44 totalCount
45 edges {
46 node {
47 playedTime
48 ...TrackItem_play
49 }
50 }
51 }
52 }
53 `,
54 queryData
55 );
56
57 const loadMoreRef = useRef<HTMLDivElement>(null);
58
59 // Subscribe to new plays
60 const subscriptionConfig: GraphQLSubscriptionConfig<AppSubscription> = {
61 subscription: graphql`
62 subscription AppSubscription {
63 fmTealAlphaFeedPlayCreated {
64 uri
65 playedTime
66 ...TrackItem_play
67 }
68 }
69 `,
70 variables: {},
71 updater: (store) => {
72 const newPlay = store.getRootField("fmTealAlphaFeedPlayCreated");
73 if (!newPlay) return;
74
75 const root = store.getRoot();
76 const connection = ConnectionHandler.getConnection(
77 root,
78 "App_fmTealAlphaFeedPlays",
79 { sortBy: [{ field: "playedTime", direction: "desc" }] }
80 );
81
82 if (!connection) return;
83
84 const edge = ConnectionHandler.createEdge(
85 store,
86 connection,
87 newPlay,
88 "FmTealAlphaFeedPlayEdge"
89 );
90
91 ConnectionHandler.insertEdgeBefore(connection, edge);
92
93 // Update totalCount
94 const totalCountRecord = root.getLinkedRecord("fmTealAlphaFeedPlays", {
95 sortBy: [{ field: "playedTime", direction: "desc" }],
96 });
97 if (totalCountRecord) {
98 const currentCount = totalCountRecord.getValue("totalCount") as number;
99 if (typeof currentCount === "number") {
100 totalCountRecord.setValue(currentCount + 1, "totalCount");
101 }
102 }
103 },
104 };
105
106 useSubscription(subscriptionConfig);
107
108 useEffect(() => {
109 window.scrollTo(0, 0);
110 }, []);
111
112 const plays =
113 data?.fmTealAlphaFeedPlays?.edges
114 ?.map((edge) => edge.node)
115 .filter((n) => n != null) || [];
116
117 useEffect(() => {
118 if (!loadMoreRef.current || !hasNext) return;
119
120 const observer = new IntersectionObserver(
121 (entries) => {
122 if (entries[0].isIntersecting && hasNext && !isLoadingNext) {
123 loadNext(20);
124 }
125 },
126 { threshold: 0.1 }
127 );
128
129 observer.observe(loadMoreRef.current);
130
131 return () => observer.disconnect();
132 }, [hasNext, isLoadingNext, loadNext]);
133
134 // Group plays by date
135 const groupedPlays: { date: string; plays: typeof plays }[] = [];
136 let currentDate = "";
137
138 plays.forEach((play) => {
139 if (!play?.playedTime) return;
140
141 const playDate = new Date(play.playedTime).toLocaleDateString("en-US", {
142 weekday: "long",
143 day: "numeric",
144 month: "long",
145 year: "numeric",
146 });
147
148 if (playDate !== currentDate) {
149 currentDate = playDate;
150 groupedPlays.push({ date: playDate, plays: [play] });
151 } else {
152 groupedPlays[groupedPlays.length - 1].plays.push(play);
153 }
154 });
155
156 return (
157 <Layout>
158 <div className="mb-8">
159 <p className="text-xs text-zinc-500 uppercase tracking-wider">
160 {data?.fmTealAlphaFeedPlays?.totalCount?.toLocaleString()} scrobbles
161 </p>
162 </div>
163
164 <div>
165 {groupedPlays.map((group, groupIndex) => (
166 <div key={groupIndex} className="mb-12">
167 <h2 className="text-xs text-zinc-600 font-medium mb-6 uppercase tracking-wider">
168 {group.date}
169 </h2>
170 <div className="space-y-1">
171 {group.plays.map((play, index) => (
172 <TrackItem key={index} play={play} />
173 ))}
174 </div>
175 </div>
176 ))}
177 </div>
178
179 {hasNext && (
180 <div ref={loadMoreRef} className="py-12 text-center">
181 {isLoadingNext ? (
182 <p className="text-xs text-zinc-600 uppercase tracking-wider">
183 Loading...
184 </p>
185 ) : (
186 <p className="text-xs text-zinc-700 uppercase tracking-wider">·</p>
187 )}
188 </div>
189 )}
190 </Layout>
191 );
192}