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, useState } from "react";
3import type { AppQuery } from "./__generated__/AppQuery.graphql";
4import type { App_plays$key } from "./__generated__/App_plays.graphql";
5import TrackItem from "./TrackItem";
6import TopAlbums from "./TopAlbums";
7import TopTracks from "./TopTracks";
8
9export default function App() {
10 const [activeTab, setActiveTab] = useState<"recent" | "tracks" | "albums">("recent");
11 const queryData = useLazyLoadQuery<AppQuery>(
12 graphql`
13 query AppQuery {
14 ...App_plays
15 }
16 `,
17 {}
18 );
19
20 const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment<
21 AppQuery,
22 App_plays$key
23 >(
24 graphql`
25 fragment App_plays on Query
26 @refetchable(queryName: "AppPaginationQuery")
27 @argumentDefinitions(
28 cursor: { type: "String" }
29 count: { type: "Int", defaultValue: 20 }
30 ) {
31 fmTealAlphaFeedPlays(
32 first: $count
33 after: $cursor
34 sortBy: [{ field: "playedTime", direction: desc }]
35 ) @connection(key: "App_fmTealAlphaFeedPlays", filters: ["sortBy"]) {
36 totalCount
37 edges {
38 node {
39 playedTime
40 ...TrackItem_play
41 }
42 }
43 }
44 }
45 `,
46 queryData
47 );
48
49 const loadMoreRef = useRef<HTMLDivElement>(null);
50
51 useEffect(() => {
52 window.scrollTo(0, 0);
53 }, []);
54
55 const plays =
56 data?.fmTealAlphaFeedPlays?.edges
57 ?.map((edge) => edge.node)
58 .filter((n) => n != null) || [];
59
60 useEffect(() => {
61 if (!loadMoreRef.current || !hasNext || activeTab !== "recent") return;
62
63 const observer = new IntersectionObserver(
64 (entries) => {
65 if (entries[0].isIntersecting && hasNext && !isLoadingNext) {
66 loadNext(20);
67 }
68 },
69 { threshold: 0.1 }
70 );
71
72 observer.observe(loadMoreRef.current);
73
74 return () => observer.disconnect();
75 }, [hasNext, isLoadingNext, loadNext, activeTab]);
76
77 // Group plays by date
78 const groupedPlays: { date: string; plays: typeof plays }[] = [];
79 let currentDate = "";
80
81 plays.forEach((play) => {
82 if (!play?.playedTime) return;
83
84 const playDate = new Date(play.playedTime).toLocaleDateString("en-US", {
85 weekday: "long",
86 day: "numeric",
87 month: "long",
88 year: "numeric",
89 });
90
91 if (playDate !== currentDate) {
92 currentDate = playDate;
93 groupedPlays.push({ date: playDate, plays: [play] });
94 } else {
95 groupedPlays[groupedPlays.length - 1].plays.push(play);
96 }
97 });
98
99 return (
100 <div className="min-h-screen bg-zinc-950 text-zinc-300 font-mono">
101 <div className="max-w-4xl mx-auto px-6 py-12">
102 <div className="mb-12 flex items-end justify-between border-b border-zinc-800 pb-6">
103 <div>
104 <h1 className="text-xs font-medium uppercase tracking-wider text-zinc-500">Listening History</h1>
105 <p className="text-xs text-zinc-600 mt-1">fm.teal.alpha.feed.play</p>
106 </div>
107
108 <div className="flex gap-4 text-xs">
109 <button
110 onClick={() => setActiveTab("recent")}
111 className={`px-2 py-1 transition-colors ${
112 activeTab === "recent"
113 ? "text-zinc-400"
114 : "text-zinc-500 hover:text-zinc-300"
115 }`}
116 >
117 Recent
118 </button>
119 <button
120 onClick={() => setActiveTab("tracks")}
121 className={`px-2 py-1 transition-colors ${
122 activeTab === "tracks"
123 ? "text-zinc-400"
124 : "text-zinc-500 hover:text-zinc-300"
125 }`}
126 >
127 Top Tracks
128 </button>
129 <button
130 onClick={() => setActiveTab("albums")}
131 className={`px-2 py-1 transition-colors ${
132 activeTab === "albums"
133 ? "text-zinc-400"
134 : "text-zinc-500 hover:text-zinc-300"
135 }`}
136 >
137 Top Albums
138 </button>
139 </div>
140 </div>
141
142 {activeTab === "recent" ? (
143 <>
144 <div className="mb-8">
145 <p className="text-xs text-zinc-500 uppercase tracking-wider">
146 {data?.fmTealAlphaFeedPlays?.totalCount?.toLocaleString()} scrobbles
147 </p>
148 </div>
149
150 <div>
151 {groupedPlays.map((group, groupIndex) => (
152 <div key={groupIndex} className="mb-12">
153 <h2 className="text-xs text-zinc-600 font-medium mb-6 uppercase tracking-wider">
154 {group.date}
155 </h2>
156 <div className="space-y-1">
157 {group.plays.map((play, index) => (
158 <TrackItem key={index} play={play} />
159 ))}
160 </div>
161 </div>
162 ))}
163 </div>
164
165 {hasNext && (
166 <div ref={loadMoreRef} className="py-12 text-center">
167 {isLoadingNext ? (
168 <p className="text-xs text-zinc-600 uppercase tracking-wider">Loading...</p>
169 ) : (
170 <p className="text-xs text-zinc-700 uppercase tracking-wider">·</p>
171 )}
172 </div>
173 )}
174 </>
175 ) : activeTab === "tracks" ? (
176 <TopTracks />
177 ) : (
178 <TopAlbums />
179 )}
180 </div>
181 </div>
182 );
183}