A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at fix/spotify 273 lines 8.3 kB view raw
1import styled from "@emotion/styled"; 2import { useSearch } from "@tanstack/react-router"; 3import { Button } from "baseui/button"; 4import { Input } from "baseui/input"; 5import { PLACEMENT, ToasterContainer } from "baseui/toast"; 6import { LabelMedium } from "baseui/typography"; 7import { useAtomValue } from "jotai"; 8import { useEffect, useState } from "react"; 9import { profileAtom } from "../atoms/profile"; 10import ScrobblesAreaChart from "../components/ScrobblesAreaChart"; 11import StickyPlayer from "../components/StickyPlayer"; 12import { API_URL } from "../consts"; 13import useProfile from "../hooks/useProfile"; 14import CloudDrive from "./CloudDrive"; 15import ExternalLinks from "./ExternalLinks"; 16import Navbar from "./Navbar"; 17import Search from "./Search"; 18import SpotifyLogin from "./SpotifyLogin"; 19 20const Container = styled.div` 21 display: flex; 22 justify-content: center; 23 height: 100vh; 24 overflow-y: auto; 25 flex-direction: row; 26`; 27 28const Flex = styled.div` 29 display: flex; 30 width: 770px; 31 margin-top: 50px; 32 flex-direction: column; 33 margin-bottom: 200px; 34`; 35 36const RightPane = styled.div` 37 @media (max-width: 1152px) { 38 display: none; 39 } 40`; 41 42const Link = styled.a` 43 text-decoration: none; 44 cursor: pointer; 45 display: block; 46 font-size: 13px; 47 48 &:hover { 49 text-decoration: underline; 50 } 51`; 52 53export type MainProps = { 54 children: React.ReactNode; 55 withRightPane?: boolean; 56}; 57 58function Main(props: MainProps) { 59 const { children } = props; 60 const withRightPane = props.withRightPane ?? true; 61 const [handle, setHandle] = useState(""); 62 const jwt = localStorage.getItem("token"); 63 const profile = useAtomValue(profileAtom); 64 const [token, setToken] = useState<string | null>(null); 65 const { did, cli } = useSearch({ strict: false }); 66 67 useEffect(() => { 68 if (did && did !== "null") { 69 localStorage.setItem("did", did); 70 71 const fetchToken = async () => { 72 try { 73 const response = await fetch(`${API_URL}/token`, { 74 method: "GET", 75 headers: { 76 "session-did": did, 77 }, 78 }); 79 const data = await response.json(); 80 localStorage.setItem("token", data.token); 81 setToken(data.token); 82 83 if (cli) { 84 await fetch("http://localhost:6996/token", { 85 method: "POST", 86 headers: { 87 "Content-Type": "application/json", 88 }, 89 body: JSON.stringify({ token: data.token }), 90 }); 91 } 92 93 if (!jwt && data.token) { 94 window.location.href = "/"; 95 } 96 } catch (e) { 97 console.error(e); 98 } 99 }; 100 fetchToken(); 101 } 102 // eslint-disable-next-line react-hooks/exhaustive-deps 103 }, []); 104 105 useProfile(token || localStorage.getItem("token")); 106 107 const onLogin = async () => { 108 if (!handle.trim()) { 109 return; 110 } 111 112 if (API_URL.includes("localhost")) { 113 window.location.href = `${API_URL}/login?handle=${handle}`; 114 return; 115 } 116 117 window.location.href = `https://rocksky.pages.dev/loading?handle=${handle}`; 118 }; 119 120 return ( 121 <Container className="bg-[var(--color-background)] text-[var(--color-text)]"> 122 <ToasterContainer 123 placement={PLACEMENT.top} 124 overrides={{ 125 ToastBody: { 126 style: { 127 zIndex: 2, 128 boxShadow: "none", 129 }, 130 }, 131 }} 132 /> 133 <Flex style={{ width: withRightPane ? "770px" : "1090px" }}> 134 <Navbar /> 135 <div 136 style={{ 137 position: "relative", 138 }} 139 > 140 {children} 141 </div> 142 </Flex> 143 {withRightPane && ( 144 <RightPane className="relative w-[300px]"> 145 <div className="fixed top-[100px] w-[300px] bg-white p-[20px]"> 146 <div className="mb-[30px]"> 147 <Search /> 148 </div> 149 {jwt && profile && !profile.spotifyConnected && <SpotifyLogin />} 150 {jwt && profile && <CloudDrive />} 151 {!jwt && ( 152 <div className="mt-[40px]"> 153 <div className="mb-[20px]"> 154 <div className="mb-[15px]"> 155 <LabelMedium className="!text-[var(--color-text)]"> 156 Bluesky handle 157 </LabelMedium> 158 </div> 159 <Input 160 name="handle" 161 startEnhancer={ 162 <div className="text-[var(--color-text-muted)] bg-[var(--color-input-background)]"> 163 @ 164 </div> 165 } 166 placeholder="<username>.bsky.social" 167 value={handle} 168 onChange={(e) => setHandle(e.target.value)} 169 overrides={{ 170 Root: { 171 style: { 172 backgroundColor: "var(--color-input-background)", 173 borderColor: "var(--color-input-background)", 174 }, 175 }, 176 StartEnhancer: { 177 style: { 178 backgroundColor: "var(--color-input-background)", 179 }, 180 }, 181 InputContainer: { 182 style: { 183 backgroundColor: "var(--color-input-background)", 184 }, 185 }, 186 Input: { 187 style: { 188 color: "var(--color-text)", 189 caretColor: "var(--color-text)", 190 }, 191 }, 192 }} 193 /> 194 </div> 195 <Button 196 onClick={onLogin} 197 overrides={{ 198 BaseButton: { 199 style: { 200 width: "100%", 201 backgroundColor: "var(--color-primary)", 202 ":hover": { 203 backgroundColor: "var(--color-primary)", 204 }, 205 ":focus": { 206 backgroundColor: "var(--color-primary)", 207 }, 208 }, 209 }, 210 }} 211 > 212 Sign In 213 </Button> 214 <LabelMedium className="text-center mt-[20px] !text-[var(--color-text-muted)]"> 215 Don't have an account? 216 </LabelMedium> 217 <div className="text-center text-[var(--color-text-muted)] "> 218 <a 219 href="https://bsky.app" 220 className="no-underline cursor-pointer !text-[var(--color-primary)]" 221 target="_blank" 222 > 223 Sign up for Bluesky 224 </a>{" "} 225 to create one now! 226 </div> 227 </div> 228 )} 229 230 <div className="mt-[40px]"> 231 <ScrobblesAreaChart /> 232 </div> 233 <ExternalLinks /> 234 <div className="inline-flex mt-[40px]"> 235 <Link 236 href="https://docs.rocksky.app/introduction-918639m0" 237 target="_blank" 238 className="mr-[10px] text-[var(--color-primary)]" 239 > 240 About 241 </Link> 242 <Link 243 href="https://docs.rocksky.app/faq-918661m0" 244 target="_blank" 245 className="mr-[10px] text-[var(--color-primary)]" 246 > 247 FAQ 248 </Link> 249 <Link 250 href="https://doc.rocksky.app/" 251 target="_blank" 252 className="mr-[10px] text-[var(--color-primary)]" 253 > 254 API Docs 255 </Link> 256 257 <Link 258 href="https://discord.gg/EVcBy2fVa3" 259 target="_blank" 260 className="text-[var(--color-primary)]" 261 > 262 Discord 263 </Link> 264 </div> 265 </div> 266 </RightPane> 267 )} 268 <StickyPlayer /> 269 </Container> 270 ); 271} 272 273export default Main;