From 0846d7190c419b61d5ee86c64effd017996097df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Torrente?= Date: Thu, 18 Apr 2024 14:18:49 -0400 Subject: [PATCH] Added Spotify now playing song in about section. --- .gitignore | 1 + package-lock.json | 153 +++++++++++++++++++++++++++ package.json | 2 + src/components/Navigation.tsx | 23 ++-- src/components/SpotifyNowPlaying.tsx | 79 ++++++++++++++ src/components/index.ts | 1 + src/constants.ts | 7 ++ src/services/spotify.ts | 74 +++++++++++++ src/types/spotify.ts | 37 +++++++ vite.config.ts | 1 - 10 files changed, 370 insertions(+), 8 deletions(-) create mode 100644 src/components/SpotifyNowPlaying.tsx create mode 100644 src/constants.ts create mode 100644 src/services/spotify.ts create mode 100644 src/types/spotify.ts diff --git a/.gitignore b/.gitignore index a547bf3..1cac559 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +.env \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b27227d..5b6a3e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ "@fontsource/roboto": "^5.0.12", "@mui/icons-material": "^5.15.15", "@mui/lab": "^5.0.0-alpha.170", + "axios": "^1.6.8", + "buffer": "^6.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-intersection-observer": "^9.8.2" @@ -2064,6 +2066,21 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", @@ -2084,6 +2101,25 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -2137,6 +2173,29 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2199,6 +2258,17 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2268,6 +2338,14 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2773,6 +2851,38 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2931,6 +3041,25 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -3195,6 +3324,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "9.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", @@ -3447,6 +3595,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index 6966d5a..61cc32b 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "@fontsource/roboto": "^5.0.12", "@mui/icons-material": "^5.15.15", "@mui/lab": "^5.0.0-alpha.170", + "axios": "^1.6.8", + "buffer": "^6.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", "react-intersection-observer": "^9.8.2" diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index 7c6deab..a5b7d27 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -1,9 +1,8 @@ import * as React from "react"; -import Tabs from "@mui/material/Tabs"; -import Tab from "@mui/material/Tab"; -import Typography from "@mui/material/Typography"; -import Box from "@mui/material/Box"; +import { Box, Tab, Tabs, Typography, Stack } from "@mui/material"; import { styled } from "@mui/system"; +import SpotifyNowPlaying from "./SpotifyNowPlaying"; +import { CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN } from "../constants"; const TabsWrapper = styled(Box)(({ theme }) => ({ flexGrow: 1, @@ -62,8 +61,8 @@ function TabPanel({ children, value, index, ...other }: TabPanelProps) { {...other} > {value === index && ( - - {children} + + {children} )} @@ -90,6 +89,7 @@ export default function Navigation() { orientation="vertical" variant="scrollable" value={value} + // (_) Param is intentionally unused. onChange={(_, newValue) => handleChange(newValue)} aria-label="Vertical tabs example" > @@ -98,7 +98,16 @@ export default function Navigation() { - About me + + + I'm a curious being, who loves challenges, people and music... + + + Projects diff --git a/src/components/SpotifyNowPlaying.tsx b/src/components/SpotifyNowPlaying.tsx new file mode 100644 index 0000000..5212adb --- /dev/null +++ b/src/components/SpotifyNowPlaying.tsx @@ -0,0 +1,79 @@ +import * as React from "react"; +import type { AuthProps } from "../types/spotify"; +import type { GetNowPlayingResponse } from "../services/spotify"; +import { + Stack, + Box, + Typography, + Card, + CardContent, + CardMedia, +} from "@mui/material"; +import { getNowPlaying } from "../services/spotify"; + +export default function SpotifyNowPlaying({ + clientId, + clientSecret, + refreshToken, +}: AuthProps) { + const [loading, setLoading] = React.useState(true); + const [nowPlayingData, setNowPlayingData] = + React.useState(null); + + React.useEffect(() => { + getNowPlaying({ clientId, clientSecret, refreshToken }) + .then((response) => { + setNowPlayingData(response); + setLoading(false); + }) + .catch((error) => { + console.error("Error fetching now playing data:", error); + }); + }, [clientId, clientSecret, refreshToken]); + + return ( + <> + {loading ? ( + Loading... + ) : nowPlayingData && nowPlayingData?.is_playing ? ( + + + + + {nowPlayingData.item.name} + + + {nowPlayingData.item?.artists + .map((_artist) => _artist.name) + .join(",")} + + + + + + + + ) : ( + You're offline. + )} + + ); +} diff --git a/src/components/index.ts b/src/components/index.ts index 0e2636e..e29525f 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1 +1,2 @@ export { default as Navigation } from "./Navigation"; +export { default as SpotifyNowPlaying } from "./SpotifyNowPlaying"; diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..3845fbe --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,7 @@ +export const TOKEN_ENDPOINT = "https://accounts.spotify.com/api/token"; +export const NOW_PLAYING_ENDPOINT = + "https://api.spotify.com/v1/me/player/currently-playing"; + +export const CLIENT_ID = import.meta.env.VITE_APP_SPOTIFY_CLIENT_ID; +export const CLIENT_SECRET = import.meta.env.VITE_APP_SPOTIFY_CLIENT_SECRET; +export const REFRESH_TOKEN = import.meta.env.VITE_APP_SPOTIFY_REFRESH_TOKEN; diff --git a/src/services/spotify.ts b/src/services/spotify.ts new file mode 100644 index 0000000..dfc7456 --- /dev/null +++ b/src/services/spotify.ts @@ -0,0 +1,74 @@ +import type { NowPlayingItem, AuthProps } from "../types/spotify"; +import axios from "axios"; +import { Buffer } from "buffer"; +import { TOKEN_ENDPOINT, NOW_PLAYING_ENDPOINT } from "../constants"; + +type GetAccessTokenResponse = { + access_token: string; + token_type: string; + expires_in: number; + refresh_toke: string; + scope: string; +}; + +const getAccessToken = async ({ + clientId, + clientSecret, + refreshToken, +}: AuthProps) => { + const basic = Buffer.from(`${clientId}:${clientSecret}`).toString("base64"); + + const bodyParams = new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: refreshToken, + }); + const body = bodyParams.toString(); + + try { + const response = await axios.post( + TOKEN_ENDPOINT, + body, + { + headers: { + Authorization: `Basic ${basic}`, + "Content-Type": "application/x-www-form-urlencoded", + }, + } + ); + return response.data; + } catch (error) { + console.error("Error getting access token:", error); + throw error; + } +}; + +export type GetNowPlayingResponse = { + is_playing: boolean; + item: NowPlayingItem; +}; + +export const getNowPlaying = async ({ + clientId, + clientSecret, + refreshToken, +}: AuthProps) => { + try { + const { access_token } = await getAccessToken({ + clientId, + clientSecret, + refreshToken, + }); + const response = await axios.get( + NOW_PLAYING_ENDPOINT, + { + headers: { + Authorization: `Bearer ${access_token}`, + }, + } + ); + return response.data; + } catch (error) { + console.error("Error getting now playing information:", error); + throw error; + } +}; diff --git a/src/types/spotify.ts b/src/types/spotify.ts new file mode 100644 index 0000000..ff10119 --- /dev/null +++ b/src/types/spotify.ts @@ -0,0 +1,37 @@ +export type AuthProps = { + clientId: string; + clientSecret: string; + refreshToken: string; +}; + +type ExternalUrls = { + spotify: string; +}; + +export type Artist = { + external_urls: ExternalUrls; + href: string; + id: string; + name: string; + type: string; + uri: string; +}; + +type Image = { + height: number; + url: string; + width: number; +}; + +type Album = { + album_type: string; + artists: Artist[]; + images: Image[]; +}; + +export type NowPlayingItem = { + name: string; + album: Album; + artists: Artist[]; + external_urls: ExternalUrls; +}; diff --git a/vite.config.ts b/vite.config.ts index 9cc50ea..081c8d9 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,6 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; -// https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], });