forked from
chadtmiller.com/slices-teal-relay
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);