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}