Repository: gilbarbara/react-spotify-web-playback
Branch: main
Commit: a900bebf2c98
Files: 79
Total size: 287.5 KB
Directory structure:
gitextract_xgcv7u9v/
├── .codesandbox/
│ └── ci.json
├── .editorconfig
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ └── config.yml
│ └── workflows/
│ └── main.yml
├── .gitignore
├── .husky/
│ ├── post-merge
│ └── pre-commit
├── .prettierignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── demo/
│ ├── package.json
│ ├── public/
│ │ ├── index.html
│ │ └── manifest.json
│ ├── src/
│ │ ├── App.tsx
│ │ ├── GitHubRepo.tsx
│ │ ├── components/
│ │ │ ├── GlobalStyles.tsx
│ │ │ ├── Player.tsx
│ │ │ ├── RepeatButton.tsx
│ │ │ └── ShuffleButton.tsx
│ │ ├── index.tsx
│ │ ├── modules/
│ │ │ ├── helpers.ts
│ │ │ └── theme.ts
│ │ └── types.ts
│ └── tsconfig.json
├── package.json
├── sonar-project.properties
├── src/
│ ├── components/
│ │ ├── Actions.tsx
│ │ ├── ClickOutside.tsx
│ │ ├── Controls.tsx
│ │ ├── Devices.tsx
│ │ ├── ErrorMessage.tsx
│ │ ├── Info.tsx
│ │ ├── Loader.tsx
│ │ ├── Player.tsx
│ │ ├── Slider.tsx
│ │ ├── SpotifyLogo.tsx
│ │ ├── Volume.tsx
│ │ ├── Wrapper.tsx
│ │ └── icons/
│ │ ├── Devices.tsx
│ │ ├── DevicesComputer.tsx
│ │ ├── DevicesMobile.tsx
│ │ ├── DevicesSpeaker.tsx
│ │ ├── Favorite.tsx
│ │ ├── FavoriteOutline.tsx
│ │ ├── Next.tsx
│ │ ├── Pause.tsx
│ │ ├── Play.tsx
│ │ ├── Previous.tsx
│ │ ├── VolumeHigh.tsx
│ │ ├── VolumeLow.tsx
│ │ ├── VolumeMid.tsx
│ │ └── VolumeMute.tsx
│ ├── constants.ts
│ ├── index.tsx
│ ├── modules/
│ │ ├── getters.ts
│ │ ├── helpers.ts
│ │ ├── hooks.ts
│ │ ├── spotify.ts
│ │ └── styled.tsx
│ └── types/
│ ├── common.ts
│ ├── index.ts
│ └── spotify.ts
├── test/
│ ├── __setup__/
│ │ ├── global.d.ts
│ │ └── vitest.setup.ts
│ ├── __snapshots__/
│ │ ├── constants.spec.ts.snap
│ │ └── index.spec.tsx.snap
│ ├── constants.spec.ts
│ ├── fixtures/
│ │ ├── data.ts
│ │ └── helpers.ts
│ ├── index.spec.tsx
│ ├── modules/
│ │ ├── __snapshots__/
│ │ │ └── getters.spec.ts.snap
│ │ ├── getters.spec.ts
│ │ ├── helpers.spec.ts
│ │ └── spotify.spec.ts
│ └── tsconfig.json
├── tsconfig.json
└── vitest.config.mts
================================================
FILE CONTENTS
================================================
================================================
FILE: .codesandbox/ci.json
================================================
{
"node": "18",
"sandboxes": ["/demo"]
}
================================================
FILE: .editorconfig
================================================
# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
[*.{scss,sass}]
indent_style = space
indent_size = 2
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: '🐛 Bug report'
description: Report a reproducible bug or regression
body:
- type: markdown
attributes:
value: |
Thank you for reporting an issue :pray:.
This issue tracker is for reporting reproducible bugs or regression's found in [react-spotify-web-playback](https://github.com/gilbarbara/react-spotify-web-playback)
If you have a question about how to achieve something and are struggling, please post a question
inside of react-spotify-web-playback's [Discussions tab](https://github.com/gilbarbara/react-spotify-web-playback/discussions)
Before submitting a new bug/issue, please check the links below to see if there is a solution or question posted there already:
- [Discussions tab](https://github.com/gilbarbara/react-spotify-web-playback/discussions)
- [Open Issues](https://github.com/gilbarbara/react-spotify-web-playback/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc)
- [Closed Issues](https://github.com/gilbarbara/react-spotify-web-playback/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed)
The more information you fill in, the better the community can help you.
- type: textarea
id: description
attributes:
label: Describe the bug
description: Provide a clear and concise description of the challenge you are running into.
validations:
required: true
- type: input
id: link
attributes:
label: Your minimal, reproducible example
description: |
Please add a link to a minimal reproduction.
Note:
- Your bug may get fixed much faster if we can run your code.
- To create a shareable code example for web, you can use CodeSandbox (https://codesandbox.io/s/new) or Stackblitz (https://stackblitz.com/).
- Please make sure the example is complete and runnable - e.g. avoid localhost URLs.
- Feel free to fork the demo CodeSandbox example to reproduce your issue: https://codesandbox.io/s/github/gilbarbara/react-spotify-web-playback/main/demo
placeholder: |
e.g. Code Sandbox, Stackblitz, etc.
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
description: Describe the steps we have to take to reproduce the behavior.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: Provide a clear and concise description of what you expected to happen.
placeholder: |
As a user, I expected ___ behavior but i am seeing ___
validations:
required: true
- type: dropdown
attributes:
label: How often does this bug happen?
description: |
Following the reproduction steps above, how easily are you able to reproduce this bug?
options:
- Every time
- Often
- Sometimes
- Only once
- type: textarea
id: screenshots_or_videos
attributes:
label: Screenshots or Videos
description: |
If applicable, add screenshots or a video to help explain your problem.
For more information on the supported file image/file types and the file size limits, please refer
to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files
placeholder: |
You can drag your video or image files inside of this editor ↓
- type: textarea
id: platform
attributes:
label: Platform
description: |
If the problem is specific to a platform, please let us know which one.
placeholder: |
- OS: [e.g. macOS, Windows, Linux, iOS, Android]
- Browser: [e.g. Chrome, Safari, Firefox, React Native]
- Version: [e.g. 91.1]
- type: input
id: rswp-version
attributes:
label: react-spotify-web-playback version
description: |
Please let us know the exact version of react-spotify-web-playback you were using when the issue occurred. Please don't just put in "latest", as this is subject to change.
placeholder: |
e.g. 0.14.0
validations:
required: true
- type: input
id: ts-version
attributes:
label: TypeScript version
description: |
If you are using TypeScript, please let us know the exact version of TypeScript you were using when the issue occurred.
placeholder: |
e.g. 5.1.0
- type: input
id: build-tool
attributes:
label: Build tool
description: |
If the issue is specific to a build tool, please let us know which one.
placeholder: |
e.g. webpack, vite, rollup, parcel, create-react-app, next.js, etc.
- type: textarea
id: additional
attributes:
label: Additional context
description: Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: 🗣 Feature Request / Question / Help
url: https://github.com/gilbarbara/react-spotify-web-playback/discussions/new
about: How does it work with...? I have an idea...
================================================
FILE: .github/workflows/main.yml
================================================
name: CI
on:
push:
branches: ['main']
tags: ['v*']
pull_request:
branches: ['*']
workflow_dispatch:
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs:
main:
name: Validate and Deploy
runs-on: ubuntu-latest
env:
CI: true
steps:
- name: Setup timezone
uses: zcong1993/setup-timezone@master
with:
timezone: America/Sao_Paulo
- name: Setup repo
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 9
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: "${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}"
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install Packages
run: pnpm install
timeout-minutes: 3
- name: Validate and Build
if: "!startsWith(github.ref, 'refs/tags/')"
run: pnpm run validate
timeout-minutes: 3
- name: SonarCloud Scan
if: "!startsWith(github.ref, 'refs/tags/')"
uses: SonarSource/sonarqube-scan-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: Publish Package
if: startsWith(github.ref, 'refs/tags/')
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
================================================
FILE: .gitignore
================================================
.idea/
.tmp/
coverage/
demo/pnpm-lock.yaml
dist/
node_modules/
================================================
FILE: .husky/post-merge
================================================
./node_modules/.bin/repo-tools install-packages
================================================
FILE: .husky/pre-commit
================================================
./node_modules/.bin/repo-tools check-remote && npm run validate
================================================
FILE: .prettierignore
================================================
coverage
lib
node_modules
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to react-spotify-web-playback
:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
**Reporting Bugs**
Before creating bug reports, please check this [list](https://github.com/gilbarbara/react-spotify-web-playback/issues) as you might find out that you don't need to create one. When you are creating a bug report, please include as many details as possible.
**Pull Requests**
Before submitting a new pull request, open a new issue to discuss it. It may already been implemented but not published or we might have found the same situation before and decide against it.
In any case:
- Format files using these rules [EditorConfig](https://github.com/gilbarbara/react-spotify-web-playback/blob/main/.editorconfig)
- Follow the [ESLint](https://github.com/gilbarbara/eslint-config) styleguide.
Thank you!
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019, Gil Barbara
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
# react-spotify-web-playback
[](https://www.npmjs.com/package/react-spotify-web-playback) [](https://github.com/gilbarbara/react-spotify-web-playback/actions/workflows/main.yml) [](https://sonarcloud.io/summary/new_code?id=gilbarbara_react-spotify-web-playback) [](https://sonarcloud.io/summary/new_code?id=gilbarbara_react-spotify-web-playback)
#### A Spotify player with [Spotify's Web Playback SDK](https://developer.spotify.com/documentation/web-playback-sdk/).
View the [demo](https://react-spotify-web-playback.gilbarbara.dev/)
Check the [supported browser](https://developer.spotify.com/documentation/web-playback-sdk/#supported-browsers) list. This library will try to use the user's devices to work with unsupported browsers.
## Setup
```bash
npm i react-spotify-web-playback
```
## Getting Started
```jsx
import SpotifyPlayer from 'react-spotify-web-playback';
;
```
### Client-side only
This library requires the `window` object.
If you are using an SSR framework, you'll need to use a [dynamic import](https://nextjs.org/docs/advanced-features/dynamic-import) or a [Client Component](https://beta.nextjs.org/docs/rendering/server-and-client-components#client-components) to load the player.
## Spotify Token
It needs a Spotify token with the following scopes:
- streaming
- user-read-email
- user-read-private
- user-read-playback-state (to read other devices' status)
- user-modify-playback-state (to update other devices)
If you want to show the Favorite button (💚), you'll need the additional scopes:
- user-library-read
- user-library-modify
Please refer to Spotify's Web API [docs](https://developer.spotify.com/documentation/web-api/) for more information.
> This library doesn't handle token generation and expiration. You'll need to handle that by yourself.
## Props
**callback** `(state: CallbackState) => void`
Get status updates from the player.
Type Definition
```typescript
type ErrorType = 'account' | 'authentication' | 'initialization' | 'playback' | 'player';
type RepeatState = 'off' | 'context' | 'track';
type Status = 'ERROR' | 'IDLE' | 'INITIALIZING' | 'READY' | 'RUNNING' | 'UNSUPPORTED';
type Type =
| 'device_update'
| 'favorite_update'
| 'player_update'
| 'progress_update'
| 'status_update'
| 'track_update';
interface CallbackState extends State {
type: Type;
}
interface State {
currentDeviceId: string;
deviceId: string;
devices: SpotifyDevice[];
error: string;
errorType: ErrorType | null;
isActive: boolean;
isInitializing: boolean;
isMagnified: boolean;
isPlaying: boolean;
isSaved: boolean;
isUnsupported: boolean;
needsUpdate: boolean;
nextTracks: SpotifyTrack[];
playerPosition: 'bottom' | 'top';
position: number;
previousTracks: SpotifyTrack[];
progressMs: number;
repeat: RepeatState;
shuffle: boolean;
status: Status;
track: SpotifyTrack;
volume: number;
}
```
**components** `CustomComponents`
Custom components for the player.
Type Definition
```typescript
interface CustomComponents {
/**
* A React component to be displayed before the previous button.
*/
leftButton?: ReactNode;
/**
* A React component to be displayed after the next button.
*/
rightButton?: ReactNode;
}
```
**getOAuthToken** `(callback: (token: string) => void) => Promise`
The callback [Spotify SDK](https://developer.spotify.com/documentation/web-playback-sdk/reference/#initializing-the-sdk) uses to get/update the token.
_Use it to generate a new token when the player needs it._
Example
```tsx
import { useState } from 'react';
import SpotifyPlayer, { Props } from 'react-spotify-web-playback';
import { refreshTokenRequest } from '../some_module';
export default function PlayerWrapper() {
const [accessToken, setAccessToken] = useState('');
const [refreshToken, setRefreshToken] = useState('');
const [expiresAt, setExpiresAt] = useState(0);
const getOAuthToken: Props['getOAuthToken'] = async callback => {
if (expiresAt > Date.now()) {
callback(accessToken);
return;
}
const { acess_token, expires_in, refresh_token } = await refreshTokenRequest(refreshToken);
setAccessToken(acess_token);
setRefreshToken(refresh_token);
setExpiresAt(Date.now() + expires_in * 1000);
callback(acess_token);
};
return ;
}
```
**getPlayer** `(player: SpotifyPlayer) => void`
Get the Spotify Web Playback SDK instance.
**hideAttribution** `boolean` ▶︎ false
Hide the Spotify logo.
**hideCoverArt** `boolean` ▶︎ false
Hide the cover art
**initialVolume** `number` between 0 and 1. ▶︎ 1
The initial volume for the player. It's not used for external devices.
**inlineVolume** `boolean` ▶︎ true
Show the volume inline for the "responsive" layout for 768px and above.
**layout** `'compact' | 'responsive'` ▶︎ 'responsive'
The layout of the player.
**locale** `Locale`
The strings used for aria-label/title attributes.
Type Definition
```typescript
interface Locale {
currentDevice?: string; // 'Current device'
devices?: string; // 'Devices'
next?: string; // 'Next'
otherDevices?: string; // 'Select other device'
pause?: string; // 'Pause'
play?: string; // 'Play'
previous?: string; // 'Previous'
removeTrack?: string; // 'Remove from your favorites'
saveTrack?: string; // 'Save to your favorites'
title?: string; // '{name} on SPOTIFY'
volume?: string; // 'Volume'
}
```
**magnifySliderOnHover**: `boolean` ▶︎ false
Magnify the player's slider on hover.
**name** `string` ▶︎ 'Spotify Web Player'
The name of the player.
**offset** `number`
The position of the list/tracks you want to start the player.
**persistDeviceSelection** `boolean` ▶︎ false
Save the device selection.
**play** `boolean`
Control the player's status.
**preloadData** `boolean`
Preload the track data before playing.
**showSaveIcon** `boolean` ▶︎ false
Display a Favorite button. It needs additional scopes in your token.
**styles** `object`
Customize the player's appearance. Check `StylesOptions` in the [types](src/types/common.ts).
**syncExternalDevice** `boolean` ▶︎ false
Use the external player context if there are no URIs and an external device is playing.
**syncExternalDeviceInterval** `number` ▶︎ 5
The time in seconds that the player will sync with external devices.
**token** `string` **REQUIRED**
A Spotify token. More info is below.
**updateSavedStatus** `(fn: (status: boolean) => any) => any`
Provide you with a function to sync the track saved status in the player.
_This works in addition to the **showSaveIcon** prop, and it is only needed if you keep track's saved status in your app._
**uris** `string | string[]` **REQUIRED**
A list of Spotify [URIs](https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids).
## Spotify API
The functions that interact with the Spotify API are exported for your convenience.
Use them at your own risk.
```tsx
import { spotifyApi } from 'react-spotify-web-playback';
```
**checkTracksStatus(token: string, tracks: string | string[]): Promise\**
**getAlbumTracks(token: string, id: string): Promise\**
**getArtistTopTracks(token: string, id: string): Promise\**
**getDevices(token: string): Promise\**
**getPlaybackState(token: string): Promise\**
**getPlaylistTracks(token: string, id: string): Promise\**
**getQueue(token: string): Promise\**
**getShow(token: string, id: string): Promise\**
**getShowEpisodes(token: string, id: string, offset = 0): Promise\**
**getTrack(token: string, id: string): Promise\**
**pause(token: string, deviceId?: string): Promise\**
**play(token: string, options: SpotifyPlayOptions): Promise\**
```typescript
interface SpotifyPlayOptions {
context_uri?: string;
deviceId: string;
offset?: number;
uris?: string[];
}
```
**previous(token: string, deviceId?: string): Promise\**
**next(token: string, deviceId?: string): Promise\**
**removeTracks(token: string, tracks: string | string[]): Promise\**
**repeat(token: string, state: 'context' | 'track' | 'off', deviceId?: string): Promise\**
**saveTracks(token: string, tracks: string | string[]): Promise\**
**seek(token: string, position: number, deviceId?: string): Promise\**
**setDevice(token: string, deviceId: string, shouldPlay?: boolean): Promise\**
**setVolume(token: string, volume: number, deviceId?: string): Promise\**
**shuffle(token: string, state: boolean, deviceId?: string): Promise\**
## Styling
You can customize the UI with a `styles` prop.
If you want a transparent player, you can use `bgColor: 'transparent'`.
Check all the available options [here](src/types/common.ts#L195).
```tsx
```

## Issues
If you find a bug, please file an issue on [our issue tracker on GitHub](https://github.com/gilbarbara/react-spotify-web-playback/issues).
## License
MIT
================================================
FILE: demo/package.json
================================================
{
"name": "react-spotify-web-playback-demo",
"version": "0.14.4",
"description": "Demo for react-spotify-web-playback",
"keywords": [],
"main": "src/index.tsx",
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@gilbarbara/hooks": "^0.9.0",
"@gilbarbara/components": "^0.15.1",
"@gilbarbara/cookies": "^1.0.1",
"@gilbarbara/eslint-config": "^0.8.4",
"@gilbarbara/helpers": "^0.9.5",
"@gilbarbara/prettier-config": "^1.0.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-scripts": "^5.0.1",
"react-spotify-web-playback": "latest"
},
"devDependencies": {
"@types/node": "^22.10.6",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"typescript": "5.7.3"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "@gilbarbara/eslint-config"
},
"prettier": "@gilbarbara/prettier-config",
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
================================================
FILE: demo/public/index.html
================================================
React Spotify Web Playback
================================================
FILE: demo/public/manifest.json
================================================
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
================================================
FILE: demo/src/App.tsx
================================================
import {
DragEventHandler,
FormEvent,
MouseEvent,
ReactNode,
useCallback,
useEffect,
useRef,
} from 'react';
import SpotifyWebPlayer, {
CallbackState,
ERROR_TYPE,
Layout,
RepeatState,
SpotifyPlayer,
STATUS,
StylesProps,
TYPE,
Type,
} from 'react-spotify-web-playback';
import {
Anchor,
Box,
Button,
ButtonGroup,
ButtonUnstyled,
Container,
Flex,
FormElementWrapper,
H1,
H4,
Icon,
Input,
Loader,
NonIdealState,
Paragraph,
Spacer,
Toggle,
} from '@gilbarbara/components';
import { request } from '@gilbarbara/helpers';
import { useEffectOnce, useSetState } from '@gilbarbara/hooks';
import GlobalStyles from './components/GlobalStyles';
import Player from './components/Player';
import RepeatButton from './components/RepeatButton';
import ShuffleButton from './components/ShuffleButton';
import {
getAuthorizeUrl,
getCredentials,
login,
logout,
parseURIs,
refreshCredentials,
setCredentials,
} from './modules/helpers';
interface State {
accessToken: string;
error?: string;
hideAttribution: boolean;
inlineVolume: boolean;
isActive: boolean;
isPlaying: boolean;
layout: 'responsive' | 'compact';
player: SpotifyPlayer | null;
refreshToken: string;
repeat: RepeatState;
shuffle: boolean;
styles?: StylesProps;
transparent: boolean;
URIs: string[];
}
const baseURIs = {
// album: 'spotify:album:0WLIcGHr0nLyKJpMirAS17', // The Breathing Effect - Mars Is A Very Bad Place For Love
album: 'spotify:album:4c7fP0tUymaZcrEFIeIeZc', // Caribou - Honey
// artist: 'spotify:artist:4oLeXFyACqeem2VImYeBFe', // Fred Again..
artist: 'spotify:artist:7A0awCXkE1FtSU8B0qwOJQ', // Jamie xx
// playlist: 'spotify:playlist:1Zr2FUPeD5hYJTGbTDSQs4', // Rework
playlist: 'spotify:playlist:3h7lEfRkEdtVvGJTdTAudn', // Nation
show: 'spotify:show:4kYCRYJ3yK5DQbP5tbfZby',
tracks: [
// Boogie
// 'spotify:track:3zYpRGnnoegSpt3SguSo3W',
// 'spotify:track:5sjeJXROHuutyj8P3JGZoN',
// 'spotify:track:3u0VPnYkZo30zw60SInouA',
// 'spotify:track:5ZoDwIP1ntHwciLjydJ8X2',
// 'spotify:track:7ohR0qPH6f2Vuj2pUNanJG',
// 'spotify:track:5g2sPpVq3hdk9ZuMfABrts',
// 'spotify:track:3mJ6pNcFM2CkykCYSREdKT',
// 'spotify:track:63DTXKZi7YdJ4tzGti1Dtr',
// 90s Electronic
// 'spotify:track:5Kh3pqvJGVCBapAgrRP8QO',
// 'spotify:track:0j5FJJOmmnXPd0XajFWkMF',
// 'spotify:track:3XWgwgbWDI56mf1Wl3cLzb',
// 'spotify:track:6rvinglzwGWPaO9N9nnHeR',
// 'spotify:track:6LERtd1yiclxFH8MHAqr0Q',
// 'spotify:track:5eFCFpmDbqGqpdOVE9CXCh',
// 'spotify:track:1RdHfWJogQm1UW4MglA8gA',
// 'spotify:track:3z70bimZB3dgdixBrxpxY0',
// 'spotify:track:3RmCwMliRzxvjGp42ItZtC',
// 'spotify:track:6WpTrVTG1mFU1hZpxbVBX7',
// 'spotify:track:5sJiLlgQKBL81QCTOkoLB5',
// 'spotify:track:7hnqJYCKZFW7vMoykaraZG',
// Dance Punk
'spotify:track:305CEVdhAViS0CW2NCLvdR',
'spotify:track:1XlDNpWy8dyEljyRd0RC2J',
'spotify:track:1Jd9W7k8DTnBSovDSxK77n',
'spotify:track:7ddGC67DasWO30q5YepUJe',
'spotify:track:3yRV0V5l87Q6EyEnv3d7YJ',
'spotify:track:7pskYSHhRTH1TFtVdQevG5',
'spotify:track:3RCj5fG55qjtmnEML1gpnA',
'spotify:track:6b9oxWgxekphG5vkz8ZpBt',
'spotify:track:29wCKit7yf8ipSCViR7cGd',
'spotify:track:0Nua2OtL0ygR9HrY50ptQX',
'spotify:track:2Yx9fXTpx1cxL6m4cMq9AO',
'spotify:track:4d2sFYYGe1vQ65IXwm6mNt',
],
};
function App() {
const URIsInput = useRef(null);
const isMounted = useRef(false);
const playerRef = useRef(null);
const code = new URLSearchParams(window.location.search).get('code');
const credentials = getCredentials();
const [
{
accessToken,
error,
hideAttribution,
inlineVolume,
isActive,
isPlaying,
layout,
refreshToken,
repeat,
shuffle,
styles,
transparent,
URIs,
},
setState,
] = useSetState({
accessToken: credentials.accessToken ?? '',
hideAttribution: false,
inlineVolume: true,
isActive: false,
isPlaying: false,
layout: 'responsive',
player: null,
refreshToken: credentials.refreshToken ?? '',
repeat: 'off',
shuffle: false,
styles: undefined,
transparent: false,
URIs: [baseURIs.artist],
});
useEffectOnce(() => {
if (code && !isMounted.current) {
login(code)
.then(spotifyCredentials => {
setCredentials(spotifyCredentials);
setState({
accessToken: spotifyCredentials.accessToken,
refreshToken: spotifyCredentials.refreshToken,
});
})
.catch(fetchError => {
setState({ error: fetchError.message || 'An error occurred. Try again' });
})
.finally(() => {
const url = new URL(window.location.href);
window.history.replaceState({}, document.title, `${url.pathname}`);
});
}
return () => {
isMounted.current = true;
};
});
useEffect(() => {
if (!playerRef.current) {
return;
}
playerRef.current.querySelector('a')?.setAttribute('draggable', `${layout === 'responsive'}`);
playerRef
.current.querySelector('img')
?.setAttribute('draggable', `${layout === 'responsive'}`);
if (layout === 'responsive') {
playerRef.current.style.left = '0';
playerRef.current.style.right = '0';
playerRef.current.style.bottom = '0';
playerRef.current.style.top = 'auto';
} else {
playerRef.current.style.left = 'auto';
playerRef.current.style.right = '20px';
playerRef.current.style.bottom = '20px';
playerRef.current.style.top = 'auto';
}
}, [layout]);
useEffect(() => {
const dragOver = (event: DragEvent) => {
event.preventDefault();
if (!event.dataTransfer) {
return;
}
event.dataTransfer.dropEffect = 'move';
};
const drop = (event: DragEvent) => {
event.preventDefault();
const offsetData = event.dataTransfer?.getData('offset');
if (!offsetData) {
return;
}
const offset = JSON.parse(offsetData);
const xPos = event.clientX - offset.x;
const yPos = event.clientY - offset.y;
if (playerRef.current) {
playerRef.current.style.left = `${xPos}px`;
playerRef.current.style.top = `${yPos}px`;
playerRef.current.style.bottom = 'auto';
playerRef.current.style.right = 'auto';
}
};
document.documentElement.addEventListener('dragover', dragOver);
document.documentElement.addEventListener('drop', drop);
return () => {
document.documentElement.removeEventListener('dragover', dragOver);
document.documentElement.removeEventListener('drop', drop);
};
}, []);
const handleSubmitURIs = useCallback(
(event: FormEvent) => {
event.preventDefault();
if (URIsInput?.current) {
setState({ URIs: parseURIs(URIsInput.current.value) });
}
},
[setState],
);
const handleClickLogout = useCallback(() => {
logout();
setState({ accessToken: '', refreshToken: '' });
}, [setState]);
const handleClickURIs = useCallback(
(event: MouseEvent) => {
event.preventDefault();
const { uris = '' } = event.currentTarget.dataset;
setState({ isPlaying: true, URIs: parseURIs(uris) });
if (URIsInput?.current) {
URIsInput.current.value = uris;
}
},
[setState],
);
const handleCallback = useCallback(
async ({ track, type, ...state }: CallbackState) => {
/* eslint-disable no-console */
console.group(`RSWP: ${type}`);
console.log(state);
console.groupEnd();
/* eslint-enable no-console */
if (type === TYPE.PLAYER) {
setState({
isActive: state.isActive,
isPlaying: state.isPlaying,
repeat: state.repeat,
shuffle: state.shuffle,
});
}
if (([TYPE.PRELOAD, TYPE.TRACK] as Array).includes(type)) {
const trackStyles = await request(
`https://scripts.gilbarbara.dev/api/getImagePlayerStyles?url=${track.image}`,
);
if (transparent) {
trackStyles.bgColor = 'transparent';
}
setState({ styles: trackStyles });
}
if (state.status === STATUS.ERROR && state.errorType === ERROR_TYPE.AUTHENTICATION) {
refreshCredentials(refreshToken)
.then(spotifyCredentials => {
setCredentials(spotifyCredentials);
setState({ accessToken: spotifyCredentials.accessToken });
})
.catch(() => {
logout();
setState({ accessToken: '', refreshToken: '' });
});
}
},
[refreshToken, setState, transparent],
);
const handlePlayerDrag: DragEventHandler = useCallback(
event => {
if (layout === 'responsive') {
return;
}
const boundingRect = playerRef.current?.getBoundingClientRect() ?? { left: 0, top: 0 };
const offset = {
x: event.clientX - boundingRect.left,
y: event.clientY - boundingRect.top,
};
event.dataTransfer.setData('offset', JSON.stringify(offset));
},
[layout],
);
const getPlayer = useCallback(
async (playerInstance: SpotifyPlayer) => {
setState({ player: playerInstance });
},
[setState],
);
const content: Record = {
connect: (
),
};
const getButtonStyle = (input: string) => {
return URIs.join(',') === input ? 'primary.300' : 'primary';
};
if (error) {
content.main = (
<>
{content.connect}
>
);
} else if (code) {
content.main = ;
} else if (accessToken) {
content.main = (
<>
}
>
Jamie xx
Caribou - Honey
Nation
Dance Punk
Syntax
Layout
setState({ layout: event.currentTarget.textContent as Layout })}
selected={layout}
size="sm"
/>
`;
================================================
FILE: test/constants.spec.ts
================================================
import { ERROR_TYPE, SPOTIFY_CONTENT_TYPE, STATUS, TRANSPARENT_COLOR, TYPE } from '~/constants';
describe('ERROR_TYPE', () => {
it('should have all options', () => {
expect(ERROR_TYPE).toMatchSnapshot();
});
});
describe('SPOTIFY_CONTENT_TYPE', () => {
it('should have all options', () => {
expect(SPOTIFY_CONTENT_TYPE).toMatchSnapshot();
});
});
describe('STATUS', () => {
it('should have all options', () => {
expect(STATUS).toMatchSnapshot();
});
});
describe('TRANSPARENT_COLOR', () => {
it('should have all options', () => {
expect(TRANSPARENT_COLOR).toBe('rgba(0, 0, 0, 0)');
});
});
describe('TYPE', () => {
it('should have all options', () => {
expect(TYPE).toMatchSnapshot();
});
});
================================================
FILE: test/fixtures/data.ts
================================================
export const playerAlbum = {
images: [
{
height: 298,
url: 'https://i.scdn.co/image/177f29ea8006359bd70784a803a21fea0360ca3e',
width: 300,
},
{
height: 64,
url: 'https://i.scdn.co/image/38ff482faf9916ca15ccb3e14b2886a27c0866e3',
width: 64,
},
{
height: 636,
url: 'https://i.scdn.co/image/10b3bd8afaf3dfa1f302b8f58e059e9802144052',
width: 640,
},
],
name: 'Trouble Man',
uri: 'spotify:album:7KvKuWUxxNPEU80c4i5AQk',
};
export const playerArtists = [
{
name: 'Marvin Gaye',
uri: 'spotify:artist:3koiLjNrgRTNbOwViDipeA',
},
];
export const playerTrack = {
album: playerAlbum,
artists: playerArtists,
duration_ms: 151626,
id: '6KUjwoHktuX3du8laPVfO8',
is_playable: true,
linked_from: {
id: null,
uri: null,
},
linked_from_uri: null,
media_type: 'audio',
name: 'Main Theme From Trouble Man',
type: 'track',
uri: 'spotify:track:6KUjwoHktuX3du8laPVfO8',
};
export const playerArtistTopTracks = {
tracks: [playerTrack],
};
export const playerAlbumTracks = {
items: [playerTrack],
};
export const playerPlaylistTracks = {
items: [{ track: playerTrack }],
};
export const playerShow = {
description: 'A Tasty Treat Podcast for Web Developers',
html_description: 'A Tasty Treat Podcast for Web Developers',
explicit: false,
external_urls: {
spotify: 'https://open.spotify.com/show/4kYCRYJ3yK5DQbP5tbfZby',
},
href: 'https://api.spotify.com/v1/shows/4kYCRYJ3yK5DQbP5tbfZby?locale=en-US%2Cen%3Bq%3D0.9%2Cpt-BR%3Bq%3D0.8%2Cpt%3Bq%3D0.7',
id: '4kYCRYJ3yK5DQbP5tbfZby',
images: [
{
height: 640,
url: 'https://i.scdn.co/image/ab6765630000ba8ada4bfc6d17ba4b7f66e6012a',
width: 640,
},
{
height: 300,
url: 'https://i.scdn.co/image/ab67656300005f1fda4bfc6d17ba4b7f66e6012a',
width: 300,
},
{
height: 64,
url: 'https://i.scdn.co/image/ab6765630000f68dda4bfc6d17ba4b7f66e6012a',
width: 64,
},
],
is_externally_hosted: false,
languages: ['en'],
media_type: 'mixed',
name: 'Syntax - Tasty Web Development Treats',
publisher: 'Wes Bos and Scott Tolinski',
type: 'show',
uri: 'spotify:show:4kYCRYJ3yK5DQbP5tbfZby',
total_episodes: 847,
episodes: {
href: 'https://api.spotify.com/v1/shows/4kYCRYJ3yK5DQbP5tbfZby/episodes?offset=0&limit=50&locale=en-US,en;q%3D0.9,pt-BR;q%3D0.8,pt;q%3D0.7',
limit: 50,
next: 'https://api.spotify.com/v1/shows/4kYCRYJ3yK5DQbP5tbfZby/episodes?offset=50&limit=50&locale=en-US,en;q%3D0.9,pt-BR;q%3D0.8,pt;q%3D0.7',
offset: 0,
previous: null,
total: 847,
items: [
{
audio_preview_url:
'https://podz-content.spotifycdn.com/audio/clips/0qbdMJHfDdYKgaV4fnOF8w/clip_2557400_2604150.mp3',
description:
' Scott and Wes unpack their experiences as electric car owners, sharing the highs and lows of making the switch. From range anxiety to charging infrastructure and cost savings, they talk about everything from the tech perks to the unexpected challenges of driving electric. Show Notes 00:00 Welcome to Syntax! 02:11 Brought to you by Sentry.io. 03:14 What cars and how long have we had them. Hyundai IONIQ 5. Tesla Model Y Long Range. 10:41 Range and dealing with range anxiety. 11:45 The EPA specs. 12:24 Things that affect range. 14:46 Charging. 17:52 Charging levels. 17:56 Level 1 charging. 19:01 Level 2 charging. 19:39 Level 3 charging. 20:10 Charging standards. 21:51 Electric car pricing. 25:56 Regenerative braking. 27:27 General maintenance. 29:04 Pricing and expenses. 31:48 Machine Gun Kelly Effect. 36:46 Would you go completely electric? 38:46 Electric-only tech. 40:57 Buying a new EV. 42:21 Edison Motors website, TikTok. Hit us up on Socials! Syntax: X Instagram Tiktok LinkedIn Threads Wes: X Instagram Tiktok LinkedIn Threads Scott: X Instagram Tiktok LinkedIn Threads Randy: X Instagram YouTube Threads',
html_description:
'
Scott and Wes unpack their experiences as electric car owners, sharing the highs and lows of making the switch. From range anxiety to charging infrastructure and cost savings, they talk about everything from the tech perks to the unexpected challenges of driving electric.