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 { useEffect, useRef } from "react";
3import type { AppQuery } from "./__generated__/AppQuery.graphql";
4import type { App_plays$key } from "./__generated__/App_plays.graphql";
5import TrackItem from "./TrackItem";
6import Layout from "./Layout";
7
8export default function App() {
9 const queryData = useLazyLoadQuery<AppQuery>(
10 graphql`
11 query AppQuery {
12 ...App_plays
13 }
14 `,
15 {}
16 );
17
18 const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment<
19 AppQuery,
20 App_plays$key
21 >(
22 graphql`
23 fragment App_plays on Query
24 @refetchable(queryName: "AppPaginationQuery")
25 @argumentDefinitions(
26 cursor: { type: "String" }
27 count: { type: "Int", defaultValue: 20 }
28 ) {
29 fmTealAlphaFeedPlays(
30 first: $count
31 after: $cursor
32 sortBy: [{ field: "playedTime", direction: desc }]
33 ) @connection(key: "App_fmTealAlphaFeedPlays", filters: ["sortBy"]) {
34 totalCount
35 edges {
36 node {
37 playedTime
38 ...TrackItem_play
39 }
40 }
41 }
42 }
43 `,
44 queryData
45 );
46
47 const loadMoreRef = useRef<HTMLDivElement>(null);
48
49 useEffect(() => {
50 window.scrollTo(0, 0);
51 }, []);
52
53 const plays =
54 data?.fmTealAlphaFeedPlays?.edges
55 ?.map((edge) => edge.node)
56 .filter((n) => n != null) || [];
57
58 useEffect(() => {
59 if (!loadMoreRef.current || !hasNext) return;
60
61 const observer = new IntersectionObserver(
62 (entries) => {
63 if (entries[0].isIntersecting && hasNext && !isLoadingNext) {
64 loadNext(20);
65 }
66 },
67 { threshold: 0.1 }
68 );
69
70 observer.observe(loadMoreRef.current);
71
72 return () => observer.disconnect();
73 }, [hasNext, isLoadingNext, loadNext]);
74
75 // Group plays by date
76 const groupedPlays: { date: string; plays: typeof plays }[] = [];
77 let currentDate = "";
78
79 plays.forEach((play) => {
80 if (!play?.playedTime) return;
81
82 const playDate = new Date(play.playedTime).toLocaleDateString("en-US", {
83 weekday: "long",
84 day: "numeric",
85 month: "long",
86 year: "numeric",
87 });
88
89 if (playDate !== currentDate) {
90 currentDate = playDate;
91 groupedPlays.push({ date: playDate, plays: [play] });
92 } else {
93 groupedPlays[groupedPlays.length - 1].plays.push(play);
94 }
95 });
96
97 return (
98 <Layout>
99 <div className="mb-8">
100 <p className="text-xs text-zinc-500 uppercase tracking-wider">
101 {data?.fmTealAlphaFeedPlays?.totalCount?.toLocaleString()} scrobbles
102 </p>
103 </div>
104
105 <div>
106 {groupedPlays.map((group, groupIndex) => (
107 <div key={groupIndex} className="mb-12">
108 <h2 className="text-xs text-zinc-600 font-medium mb-6 uppercase tracking-wider">
109 {group.date}
110 </h2>
111 <div className="space-y-1">
112 {group.plays.map((play, index) => (
113 <TrackItem key={index} play={play} />
114 ))}
115 </div>
116 </div>
117 ))}
118 </div>
119
120 {hasNext && (
121 <div ref={loadMoreRef} className="py-12 text-center">
122 {isLoadingNext ? (
123 <p className="text-xs text-zinc-600 uppercase tracking-wider">Loading...</p>
124 ) : (
125 <p className="text-xs text-zinc-700 uppercase tracking-wider">·</p>
126 )}
127 </div>
128 )}
129 </Layout>
130 );
131}