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}