A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at fix/spotify 369 lines 12 kB view raw
1import styled from "@emotion/styled"; 2import { Copy } from "@styled-icons/ionicons-outline"; 3import { useQuery } from "@tanstack/react-query"; 4import { Link, useNavigate } from "@tanstack/react-router"; 5import { Avatar } from "baseui/avatar"; 6import { Checkbox, LABEL_PLACEMENT, STYLE_TYPE } from "baseui/checkbox"; 7import { NestedMenus, StatefulMenu } from "baseui/menu"; 8import { Modal, ModalBody, ModalHeader } from "baseui/modal"; 9import { PLACEMENT, StatefulPopover } from "baseui/popover"; 10import { DURATION, useSnackbar } from "baseui/snackbar"; 11import { StatefulTooltip } from "baseui/tooltip"; 12import { LabelMedium } from "baseui/typography"; 13import copy from "copy-to-clipboard"; 14import { useAtom, useAtomValue, useSetAtom } from "jotai"; 15import numeral from "numeral"; 16import * as R from "ramda"; 17import { useEffect, useMemo, useState } from "react"; 18import { profileAtom } from "../../atoms/profile"; 19import { themeAtom } from "../../atoms/theme"; 20import { API_URL } from "../../consts"; 21import { useProfileStatsByDidQuery } from "../../hooks/useProfile"; 22 23const Container = styled.div` 24 position: fixed; 25 top: 0; 26 width: 1090px; 27 z-index: 1; 28 display: flex; 29 justify-content: space-between; 30 align-items: center; 31 height: 80px; 32 33 @media (max-width: 1152px) { 34 width: 100%; 35 padding: 0 20px; 36 } 37`; 38 39export const Code = styled.div` 40 background-color: #000; 41 color: #fff; 42 padding: 5px; 43 display: inline-block; 44 border-radius: 5px; 45`; 46 47function Navbar() { 48 const [isOpen, setIsOpen] = useState(false); 49 const [{ darkMode }, setTheme] = useAtom(themeAtom); 50 const setProfile = useSetAtom(profileAtom); 51 const profile = useAtomValue(profileAtom); 52 const navigate = useNavigate(); 53 const jwt = localStorage.getItem("token"); 54 const { enqueue } = useSnackbar(); 55 const profileStats = useProfileStatsByDidQuery( 56 R.propOr(undefined, "did", profile), 57 ); 58 59 const { data } = useQuery({ 60 queryKey: ["webscrobbler"], 61 queryFn: async () => { 62 const response = await fetch(`${API_URL}/webscrobbler`, { 63 method: "GET", 64 headers: { 65 Authorization: `Bearer ${jwt}`, 66 }, 67 }); 68 return response.json(); 69 }, 70 }); 71 72 const webscrobblerWebhook = useMemo(() => { 73 if (data) { 74 return `https://webscrobbler.rocksky.app/${data.uuid}`; 75 } 76 return ""; 77 }, [data]); 78 79 useEffect(() => { 80 if (profile?.spotifyConnected && !!localStorage.getItem("spotify")) { 81 localStorage.removeItem("spotify"); 82 enqueue( 83 { 84 message: "Spotify account connected successfully!", 85 }, 86 DURATION.long, 87 ); 88 } 89 }, [enqueue, profile]); 90 91 const close = () => { 92 setIsOpen(false); 93 }; 94 95 return ( 96 <Container className="bg-[var(--color-background)] text-[var(--color-text)]"> 97 <div> 98 <Link to="/" style={{ textDecoration: "none" }}> 99 <h2 className="text-[var(--color-primary)] text-[26px] font-bold"> 100 Rocksky 101 </h2> 102 </Link> 103 </div> 104 105 {profile && jwt && ( 106 <StatefulPopover 107 placement={PLACEMENT.bottomRight} 108 overrides={{ 109 Body: { 110 style: { 111 zIndex: 2, 112 backgroundColor: "var(--color-background)", 113 width: "282px", 114 }, 115 }, 116 }} 117 content={({ close }) => ( 118 <div className="border-[var(--color-border)] border-[1px] pt-[20px] pb-[20px] bg-[var(--color-background)] rounded-[6px]"> 119 <div> 120 <div className="flex items-center justify-center bg-[var(--color-background)] pl-[20px] pr-[20px]"> 121 <div className="flex flex-col items-center"> 122 <div className="mb-[5px]"> 123 <Link to="/profile/$did" params={{ did: profile.handle }}> 124 <Avatar 125 src={profile.avatar} 126 name={profile.displayName} 127 size="80px" 128 /> 129 </Link> 130 </div> 131 132 <Link 133 to="/profile/$did" 134 params={{ did: profile.handle }} 135 className="no-underline" 136 > 137 <LabelMedium className="text-center text-[20px] !text-[var(--color-text)]"> 138 {profile.displayName} 139 </LabelMedium> 140 </Link> 141 <a 142 href={`https://bsky.app/profile/${profile.handle}`} 143 target="_blank" 144 className="no-underline" 145 > 146 <LabelMedium 147 color="var(--color-primary)" 148 className="text-center" 149 > 150 @{profile.handle} 151 </LabelMedium> 152 </a> 153 154 <div className="flex flex-row mt-[5px]"> 155 <LabelMedium 156 margin={0} 157 color="var(--color-text-muted)" 158 className="text-center !mr-[5px]" 159 > 160 {numeral(profileStats.data.scrobbles).format("0,0")} 161 </LabelMedium> 162 <LabelMedium color="var(--color-text-muted)"> 163 scrobbles 164 </LabelMedium> 165 </div> 166 </div> 167 </div> 168 </div> 169 <NestedMenus> 170 <StatefulMenu 171 items={[ 172 { 173 id: "api-applications", 174 label: ( 175 <LabelMedium className="!text-[var(--color-text)]"> 176 API Applications 177 </LabelMedium> 178 ), 179 }, 180 { 181 id: "webscrobbler", 182 label: ( 183 <LabelMedium className="!text-[var(--color-text)]"> 184 Web Scrobbler 185 </LabelMedium> 186 ), 187 }, 188 { 189 id: "dark-mode", 190 label: ( 191 <div className="flex flex-row items-center"> 192 <LabelMedium className="!text-[var(--color-text)] flex-1"> 193 Dark Mode 194 </LabelMedium> 195 <Checkbox 196 checked={darkMode} 197 checkmarkType={STYLE_TYPE.toggle_round} 198 onChange={(e) => { 199 setTheme({ 200 darkMode: e.target.checked, 201 }); 202 localStorage.setItem( 203 "darkMode", 204 e.target.checked ? "true" : "false", 205 ); 206 }} 207 labelPlacement={LABEL_PLACEMENT.right} 208 overrides={{ 209 Toggle: { 210 style: { 211 backgroundColor: "#fff", 212 }, 213 }, 214 ToggleTrack: { 215 style: { 216 backgroundColor: "var(--color-toggle-track)", 217 }, 218 }, 219 }} 220 /> 221 </div> 222 ), 223 }, 224 { 225 id: "signout", 226 label: ( 227 <LabelMedium className="!text-[var(--color-text)]"> 228 Sign out 229 </LabelMedium> 230 ), 231 }, 232 ]} 233 onItemSelect={({ item }) => { 234 switch (item.id) { 235 case "profile": 236 navigate({ 237 to: "/profile/$did", 238 params: { did: profile.handle }, 239 }); 240 break; 241 case "api-applications": 242 navigate({ 243 to: "/apikeys", 244 }); 245 break; 246 case "signout": 247 setProfile(null); 248 localStorage.removeItem("token"); 249 localStorage.removeItem("did"); 250 window.location.href = "/"; 251 break; 252 case "webscrobbler": 253 setIsOpen(true); 254 break; 255 case "dark-mode": 256 return; 257 default: 258 break; 259 } 260 close(); 261 }} 262 overrides={{ 263 List: { 264 style: { 265 boxShadow: "none", 266 backgroundColor: "var(--color-background)", 267 }, 268 }, 269 Option: { 270 style: { 271 height: "44px", 272 }, 273 }, 274 ListItem: { 275 style: ({ $isHighlighted }) => ({ 276 backgroundColor: $isHighlighted 277 ? "var(--color-menu-hover)" 278 : "var(--color-background)", 279 color: "var(--color-text)", 280 }), 281 }, 282 }} 283 /> 284 </NestedMenus> 285 </div> 286 )} 287 > 288 <button 289 style={{ 290 border: "none", 291 backgroundColor: "transparent", 292 cursor: "pointer", 293 }} 294 > 295 <Avatar 296 src={profile.avatar} 297 name={profile.displayName} 298 size="scale1200" 299 /> 300 </button> 301 </StatefulPopover> 302 )} 303 304 <Modal 305 onClose={close} 306 isOpen={isOpen} 307 overrides={{ 308 Root: { 309 style: { 310 zIndex: 1, 311 }, 312 }, 313 Dialog: { 314 style: { 315 backgroundColor: "var(--color-background)", 316 }, 317 }, 318 Close: { 319 style: { 320 color: "var(--color-text)", 321 ":hover": { 322 color: "var(--color-text)", 323 opacity: 0.8, 324 }, 325 }, 326 }, 327 }} 328 size={650} 329 > 330 <ModalHeader className="!text-[var(--color-text)]"> 331 Setup Web Scrobbler 332 </ModalHeader> 333 <ModalBody> 334 <LabelMedium className="!text-[var(--color-text)]"> 335 To use the Web Scrobbler, you need to install the browser extension 336 and connect it to Rocksky. 337 </LabelMedium> 338 <div className="mt-[20px]"> 339 <a 340 href="https://github.com/web-scrobbler/web-scrobbler" 341 target="_blank" 342 rel="noopener noreferrer" 343 className="text-[var(--color-primary)]" 344 > 345 Install Web Scrobbler 346 </a> 347 </div> 348 <div className="mt-[20px]"> 349 <LabelMedium className="!text-[var(--color-text)]"> 350 After installing the extension, add the following URL to the 351 extension settings as a custom API URL: 352 </LabelMedium> 353 <Code className="mt-[15px]">{webscrobblerWebhook}</Code> 354 <StatefulTooltip content="Copy API Key"> 355 <Copy 356 onClick={() => copy(webscrobblerWebhook)} 357 size={18} 358 color="var(--color-text)" 359 className="ml-[5px] cursor-pointer" 360 /> 361 </StatefulTooltip> 362 </div> 363 </ModalBody> 364 </Modal> 365 </Container> 366 ); 367} 368 369export default Navbar;