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}