Teal.fm frontend powered by slices.network tealfm-slices.wisp.place
tealfm slices
1import { StrictMode, Suspense } from "react"; 2import { createRoot } from "react-dom/client"; 3import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; 4import "./index.css"; 5import App from "./App.tsx"; 6import Profile from "./Profile.tsx"; 7import TopTracks from "./TopTracks.tsx"; 8import TopAlbums from "./TopAlbums.tsx"; 9import LoadingFallback from "./LoadingFallback.tsx"; 10import { RelayEnvironmentProvider } from "react-relay"; 11import { 12 Environment, 13 type FetchFunction, 14 type GraphQLResponse, 15 Network, 16 Observable, 17 type SubscribeFunction, 18} from "relay-runtime"; 19import { createClient } from "graphql-ws"; 20import Overall from "./Overall.tsx"; 21import ProfileTopArtists from "./ProfileTopArtists.tsx"; 22import ProfileTopAlbums from "./ProfileTopAlbums.tsx"; 23import ProfileTopTracks from "./ProfileTopTracks.tsx"; 24import TopArtists from "./TopArtists.tsx"; 25 26const HTTP_ENDPOINT = 27 "https://api.slices.network/graphql?slice=at://did:plc:n2sgrmrxjell7f5oa5ruwlyl/network.slices.slice/3m5d5dfs3oy26"; 28 29const WS_ENDPOINT = 30 "wss://api.slices.network/graphql/ws?slice=at://did:plc:n2sgrmrxjell7f5oa5ruwlyl/network.slices.slice/3m5d5dfs3oy26"; 31 32const fetchGraphQL: FetchFunction = async (request, variables) => { 33 const resp = await fetch(HTTP_ENDPOINT, { 34 method: "POST", 35 headers: { "Content-Type": "application/json" }, 36 body: JSON.stringify({ query: request.text, variables }), 37 }); 38 if (!resp.ok) { 39 throw new Error("Response failed."); 40 } 41 return await resp.json(); 42}; 43 44const wsClient = createClient({ 45 url: WS_ENDPOINT, 46 retryAttempts: 5, 47 shouldRetry: () => true, 48 on: { 49 connected: () => { 50 console.log("WebSocket connected!"); 51 }, 52 error: (error) => { 53 console.error("WebSocket error:", error); 54 }, 55 closed: (event) => { 56 console.log("WebSocket closed:", event); 57 }, 58 }, 59}); 60 61const subscribe: SubscribeFunction = (operation, variables) => { 62 return Observable.create((sink) => { 63 if (!operation.text) { 64 sink.error(new Error("Missing operation text")); 65 return; 66 } 67 68 return wsClient.subscribe( 69 { 70 operationName: operation.name, 71 query: operation.text, 72 variables, 73 }, 74 { 75 next: (data) => { 76 if (data.data !== null && data.data !== undefined) { 77 sink.next({ data: data.data } as GraphQLResponse); 78 } 79 }, 80 error: (error) => { 81 console.error("Subscription error:", error); 82 if (error instanceof Error) { 83 sink.error(error); 84 } else if (error instanceof CloseEvent) { 85 sink.error( 86 new Error(`WebSocket closed: ${error.code} ${error.reason}`), 87 ); 88 } else { 89 sink.error(new Error(JSON.stringify(error))); 90 } 91 }, 92 complete: () => sink.complete(), 93 }, 94 ); 95 }); 96}; 97 98const environment = new Environment({ 99 network: Network.create(fetchGraphQL, subscribe), 100}); 101 102createRoot(document.getElementById("root")!).render( 103 <StrictMode> 104 <BrowserRouter> 105 <RelayEnvironmentProvider environment={environment}> 106 <Suspense fallback={<LoadingFallback />}> 107 <Routes> 108 <Route path="/" element={<App />} /> 109 <Route path="/artists" element={<TopArtists />} /> 110 <Route path="/artists/:period" element={<TopArtists />} /> 111 <Route path="/tracks" element={<TopTracks />} /> 112 <Route path="/tracks/:period" element={<TopTracks />} /> 113 <Route path="/albums" element={<TopAlbums />} /> 114 <Route path="/albums/:period" element={<TopAlbums />} /> 115 <Route 116 path="/profile/:handle" 117 element={<Navigate to="scrobbles" replace />} 118 /> 119 <Route path="/profile/:handle/scrobbles" element={<Profile />} /> 120 <Route path="/profile/:handle/overall" element={<Overall />}> 121 <Route index element={<Navigate to="artists" replace />} /> 122 <Route path="artists" element={<ProfileTopArtists />} /> 123 <Route path="artists/:period" element={<ProfileTopArtists />} /> 124 <Route path="albums" element={<ProfileTopAlbums />} /> 125 <Route path="albums/:period" element={<ProfileTopAlbums />} /> 126 <Route path="tracks" element={<ProfileTopTracks />} /> 127 <Route path="tracks/:period" element={<ProfileTopTracks />} /> 128 </Route> 129 </Routes> 130 </Suspense> 131 </RelayEnvironmentProvider> 132 </BrowserRouter> 133 </StrictMode>, 134);