├── .gitignore
├── README.md
├── blob
├── body.png
├── login.png
└── slug.png
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── manifest.json
└── robots.txt
├── server
└── server.js
└── src
├── App.css
├── App.js
├── data
├── DataLayer.js
├── SoundLayer.js
├── reducer.js
└── soundReducer.js
├── index.css
├── index.js
├── login
├── Login.css
└── Login.js
├── player
├── Player.css
├── Player.js
├── body
│ ├── Body.css
│ ├── Body.js
│ └── partials
│ │ ├── Header.css
│ │ ├── Header.js
│ │ ├── SongRow.css
│ │ └── SongRow.js
├── footer
│ ├── Footer.css
│ └── Footer.js
└── sidebar
│ ├── Sidebar.css
│ ├── Sidebar.js
│ └── partials
│ ├── SidebarOption.css
│ └── SidebarOption.js
├── serviceWorker.js
└── spotify.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Spotify React Clone
2 |
3 | This app is a demo replica of Spotify (using their spotify api) that I decided to create in order to learn React and also have fun. This is not meant for production use, just to have fun locally.
4 |
5 | ## Project Status
6 |
7 | This project is currently in development. There are many features still not implemented and I will try to add as many as I can, and as many as I know how to do, due to being not experienced in React
8 |
9 | ## Project Screen Shot(s)
10 |
11 | ### Login Page
12 |
13 | 
14 |
15 | ### Main Page
16 |
17 | 
18 |
19 | ### Dynamic Slugs
20 |
21 | 
22 |
23 | ## Installation and Setup Instructions
24 |
25 | #### Example:
26 |
27 | Before the already well known react installation process, place visit spotify developer page and go inside the dashboard. There you can create a new app, and you will get your clientId. Also open the app inside the dashboard and edit settings. Only thing you need to change is redirect url. It is the url your app is running at. By default localhost runs at http://localhost:3000/ so you can copy and paste that if you are just looking to test the app.
28 |
29 | Copy your clientId inside `spotify.js` file and also put your `redirectUrl`
30 |
31 | Now for the regular react app process you will need `node` and `npm` installed globally on your machine.
32 |
33 | Installation:
34 |
35 | `npm install`
36 |
37 | To Start Server:
38 |
39 | `npm run build && npm start`
40 |
41 | To Visit App:
42 |
43 | `localhost:3000`
44 |
45 | ## How does it work
46 |
47 | Once the app is started and you have successfully added your clientId and redirectUrl, on start you should see the login page. That's because you haven't got the authorization token from spotify api. Once you click login, spotify takes it from there and asks you to login to you account. You get redirected back with the token I store inside React Context state. With that token I can reach inside spotify api and get your playlists. (You should see them pop to the left). You can pick whatever playlist you want, and start the songs.
48 |
49 | **Important Notice** -> Spotify only adds the preview_url to their songs, so you can only play 30 sec of every song. Some don't even have the `preview_url` and will not start at all. Sucks, I know, but that's why this project is only created for fun. I wanted to interact with a api online, and spotify api looked really nice. So this app is never ment to be used on production, but just as a little project to hopefully get you started with React. I know it did help me so hopefully I will be able to help someone else :)
50 |
51 | ## Reflection
52 |
53 | So at this point I just want to reflect on this app and why I decided to use it. So I have been working as a software developer for the last 2 years, and my main focus has been more on the backend side of things. So creating api's, microservices, interacting with databases (nosql, sql, key value stores, etc.) has always been my main focus.
54 |
55 | Recently I decided to switch gears and try learning more about frontend and devops things. I picked up dockers, started learning them, and after a while got pretty good at it (this app will potentially be dockerized). When it comes to frontend I picked up Vue.js pretty fast as it is probably the fastes and easiest frontend language to start with. I created bunch of little small apps, just testing how everything works, how lifecycles works, how it all connects to Vuex, etc.
56 |
57 | Now I decided it would not be the worst idea to try something new (like React) so I can compare the two frameworks and have a better grasp on how they both work. Advantages and disadvantages of both.
58 |
59 | You can clearly see by my code, that I am no expert when it comes to frontend, so please bare with me while I am trying to improve the code and add new featues. While it might take only few minutes to some, I still have to spend some time on stackoverflow and alike to seek help whenever I get into trouble.
60 |
61 | And as said before, this is an app in development and there are many known issues and bugs that I will eventually fix.
62 |
--------------------------------------------------------------------------------
/blob/body.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fvukojevic/Spotify-React-Clone/e227ec689735fd6baf05f40ac85146dd9d346024/blob/body.png
--------------------------------------------------------------------------------
/blob/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fvukojevic/Spotify-React-Clone/e227ec689735fd6baf05f40ac85146dd9d346024/blob/login.png
--------------------------------------------------------------------------------
/blob/slug.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fvukojevic/Spotify-React-Clone/e227ec689735fd6baf05f40ac85146dd9d346024/blob/slug.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "spotify-clone-react",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@material-ui/core": "^4.11.0",
7 | "@material-ui/icons": "^4.9.1",
8 | "@testing-library/jest-dom": "^4.2.4",
9 | "@testing-library/react": "^9.3.2",
10 | "@testing-library/user-event": "^7.1.2",
11 | "react": "^16.13.1",
12 | "react-dom": "^16.13.1",
13 | "react-scripts": "3.4.1",
14 | "spotify-web-api-js": "^1.5.0"
15 | },
16 | "scripts": {
17 | "start": "node server/server.js",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject"
21 | },
22 | "eslintConfig": {
23 | "extends": "react-app"
24 | },
25 | "browserslist": {
26 | "production": [
27 | ">0.2%",
28 | "not dead",
29 | "not op_mini all"
30 | ],
31 | "development": [
32 | "last 1 chrome version",
33 | "last 1 firefox version",
34 | "last 1 safari version"
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fvukojevic/Spotify-React-Clone/e227ec689735fd6baf05f40ac85146dd9d346024/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
16 |
17 |
26 | Spotify Clone
27 |
28 |
29 | You need to enable JavaScript to run this app.
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Spotify Clone",
3 | "name": "Create Spotify clone App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const express = require('express');
3 | const app = express();
4 | const publicPath = path.join(__dirname, '..', 'build');
5 | const port = process.env.PORT || 3000;
6 |
7 | app.use(express.static(publicPath));
8 | app.get('*', (req, res) => {
9 | res.sendFile(path.join(publicPath, 'index.html'));
10 | });
11 | app.listen(port, () => {
12 | console.log('Server is up!');
13 | });
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fvukojevic/Spotify-React-Clone/e227ec689735fd6baf05f40ac85146dd9d346024/src/App.css
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, {useEffect} from 'react';
2 | import Login from './login/Login';
3 | import './App.css';
4 | import {getAccessTokenFromUrl} from "./spotify";
5 | import SpotifyWebApi from 'spotify-web-api-js';
6 | import Player from "./player/Player";
7 | import {useDataLayerValue} from './data/DataLayer';
8 |
9 | const spotify = new SpotifyWebApi();
10 |
11 | function App() {
12 | const [{token}, dispatch] = useDataLayerValue();
13 |
14 | useEffect(() => {
15 | const hash = getAccessTokenFromUrl();
16 | window.location.hash = '';
17 | const _token = hash['access_token'];
18 |
19 | if (_token) {
20 | dispatch({
21 | type: "SET_TOKEN",
22 | token: _token
23 | });
24 |
25 | spotify.setAccessToken(_token);
26 | spotify.getMe().then(user => {
27 | dispatch({
28 | type: 'SET_USER',
29 | user: user
30 | })
31 | });
32 |
33 | spotify.getUserPlaylists().then((playlists) => {
34 | dispatch({
35 | type: 'SET_PLAYLISTS',
36 | playlists: playlists,
37 | })
38 | });
39 | }
40 | }, []);
41 | return ;
42 | }
43 |
44 | export default App;
45 |
--------------------------------------------------------------------------------
/src/data/DataLayer.js:
--------------------------------------------------------------------------------
1 | import React, {createContext, useContext, useReducer} from 'react';
2 |
3 | export const DataLayerContext = createContext();
4 |
5 | export const DataLayer = ({initialState, reducer, children}) => (
6 |
7 | {children}
8 |
9 | );
10 |
11 | export const useDataLayerValue = () => useContext(DataLayerContext);
--------------------------------------------------------------------------------
/src/data/SoundLayer.js:
--------------------------------------------------------------------------------
1 | import React, {createContext, useContext, useReducer} from 'react';
2 |
3 | export const SoundLayerContext = createContext();
4 |
5 | export const SoundLayer = ({initialState, reducer, children}) => (
6 |
7 | {children}
8 |
9 | );
10 |
11 | export const useSoundLayerValue = () => useContext(SoundLayerContext);
--------------------------------------------------------------------------------
/src/data/reducer.js:
--------------------------------------------------------------------------------
1 | export const initialState = {
2 | user: null,
3 | playlists: [],
4 | playing: false,
5 | item: null,
6 | current_playlist: null,
7 | tracks: null,
8 | track: null,
9 | token: null,
10 | };
11 |
12 | const reducer = (state, action) => {
13 | // Action -> type, [payload]
14 | switch (action.type) {
15 | case 'SET_USER':
16 | return {
17 | ...state,
18 | user: action.user
19 | };
20 | case "SET_TOKEN": {
21 | return {
22 | ...state,
23 | token: action.token
24 | }
25 | }
26 | case "SET_PLAYLISTS": {
27 | return {
28 | ...state,
29 | playlists: action.playlists
30 | }
31 | }
32 | case 'SET_CURRENT_PLAYLIST': {
33 | let currentPlaylist = null;
34 | state.playlists.items.forEach(playlist => {
35 | if(playlist.id === action.id) {
36 | currentPlaylist = playlist;
37 | }
38 | });
39 | return {
40 | ...state,
41 | current_playlist: currentPlaylist
42 | }
43 | }
44 | case 'SET_TRACKS': {
45 | return {
46 | ...state,
47 | tracks: action.tracks
48 | };
49 | }
50 | case 'SET_TRACK': {
51 | return {
52 | ...state,
53 | track: action.track
54 | };
55 | }
56 | default:
57 | return state;
58 | }
59 | };
60 |
61 | export default reducer;
--------------------------------------------------------------------------------
/src/data/soundReducer.js:
--------------------------------------------------------------------------------
1 | export const soundInitialState = {
2 | audio: null,
3 | playing: false,
4 | volume: 0.3,
5 | repeat: false,
6 | shuffle: false,
7 | };
8 |
9 | const soundReducer = (state, action) => {
10 | switch (action.type) {
11 | case 'SET_AUDIO':
12 | return {
13 | ...state,
14 | audio: action.audio
15 | };
16 | case 'SET_PLAYING':
17 | if(!action.playing) {
18 | if(state.audio) {
19 | state.audio.pause();
20 | }
21 | } else {
22 | if(state.audio) {
23 | state.audio.play();
24 | }
25 | }
26 | return {
27 | ...state,
28 | playing: action.playing
29 | };
30 | case 'SET_VOLUME':
31 | if(state.audio) {
32 | state.audio.volume = action.volume;
33 | }
34 | return {
35 | ...state,
36 | volume: action.volume,
37 | };
38 | case 'SET_REPEAT':
39 | if(state.audio) {
40 | state.audio.loop = action.repeat;
41 | }
42 | return {
43 | ...state,
44 | repeat: action.repeat,
45 | };
46 | case 'SET_SHUFFLE':
47 | if(state.audio) {
48 | return {
49 | ...state,
50 | shuffle: action.shuffle
51 | };
52 | }
53 | return state;
54 | default:
55 | return state;
56 | }
57 | };
58 |
59 | export default soundReducer;
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | }
4 |
5 | body::-webkit-scrollbar{
6 | display:none;
7 | }
8 |
9 | body {
10 | margin: 0;
11 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
12 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
13 | sans-serif;
14 | -webkit-font-smoothing: antialiased;
15 | -moz-osx-font-smoothing: grayscale;
16 | }
17 |
18 | code {
19 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
20 | monospace;
21 | }
22 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 | import {DataLayer} from "./data/DataLayer";
7 | import reducer, {initialState} from "./data/reducer";
8 | import {SoundLayer} from "./data/SoundLayer";
9 | import soundReducer, {soundInitialState} from "./data/soundReducer";
10 |
11 | ReactDOM.render(
12 |
13 |
14 |
15 |
16 |
17 |
18 | ,
19 | document.getElementById('root')
20 | );
21 |
22 | // If you want your app to work offline and load faster, you can change
23 | // unregister() to register() below. Note this comes with some pitfalls.
24 | // Learn more about service workers: https://bit.ly/CRA-PWA
25 | serviceWorker.unregister();
26 |
--------------------------------------------------------------------------------
/src/login/Login.css:
--------------------------------------------------------------------------------
1 | .login {
2 | display: grid;
3 | place-items: center;
4 | height: 100vh;
5 | background-color: black;
6 | }
7 |
8 | .login > img {
9 | width: 100%
10 | }
11 |
12 | .login a {
13 | padding: 20px;
14 | background-color: #1db954;
15 | border-radius: 99px;
16 | font-weight: 800;
17 | color: white;
18 | text-decoration: none;
19 | }
--------------------------------------------------------------------------------
/src/login/Login.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './Login.css';
3 | import { loginUrl } from '../spotify'
4 |
5 | function Login() {
6 | return (
7 |
13 | )
14 | }
15 |
16 | export default Login
--------------------------------------------------------------------------------
/src/player/Player.css:
--------------------------------------------------------------------------------
1 | .player_body {
2 | display:flex;
3 | }
--------------------------------------------------------------------------------
/src/player/Player.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './Player.css'
3 | import Sidebar from "./sidebar/Sidebar";
4 | import Body from "./body/Body";
5 | import Footer from "./footer/Footer";
6 |
7 | function Player({ spotify }) {
8 | return (
9 |
14 |
15 |
16 | )
17 | }
18 |
19 | export default Player
--------------------------------------------------------------------------------
/src/player/body/Body.css:
--------------------------------------------------------------------------------
1 | .body {
2 | flex: 0.8;
3 | height: 100vh;
4 | color: #fff;
5 | padding: 30px;
6 | overflow-y: overlay;
7 | background: linear-gradient(rgb(91, 87, 115), rgba(0, 0, 0, 1));
8 | }
9 |
10 | .body::-webkit-scrollbar{
11 | display: none;
12 | }
13 |
14 | .body__info {
15 | display: flex;
16 | align-items: flex-end;
17 | padding: 10px;
18 | }
19 |
20 | .body__info > img {
21 | height: 20vw;
22 | margin: 0 20px;
23 | box-shadow: 0 4px 60px rgba(0, 0, 0, 60);
24 | }
25 |
26 | .body__icons {
27 | display: flex;
28 | align-items: center;
29 | }
30 |
31 | .body__icons > .MuiSvgIcon-root{
32 | margin-right: 20px;
33 | }
34 |
35 | .body__shuffle {
36 | font-size: 80px!important;
37 | margin-left: 50px;
38 | margin-top: 20px;
39 | margin-bottom: 20px;
40 | }
41 |
42 | .body__shuffle:hover {
43 | transition: 100ms transform ease-in;
44 | transform: scale(1.08);
45 | }
46 |
47 | .body__infoText {
48 | flex: 1
49 | }
50 |
51 | .body__infoText > h2 {
52 | font-size: 48px;
53 | margin-bottom: 10px
54 | }
55 |
56 | .body__infoText > p {
57 | font-size: 14px;
58 | }
59 |
60 | .body__songs {
61 | margin: 20px -30px;
62 | }
--------------------------------------------------------------------------------
/src/player/body/Body.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './Body.css'
3 | import Header from "./partials/Header";
4 | import {useDataLayerValue} from "../../data/DataLayer";
5 | import PlayCircleFilledIcon from '@material-ui/icons/PlayCircleFilled';
6 | import PauseCircleFilledIcon from "@material-ui/icons/PauseCircleFilled";
7 | import FavoriteIcon from '@material-ui/icons/Favorite';
8 | import MoreHorizIcon from '@material-ui/icons/MoreHoriz';
9 | import SongRow from "./partials/SongRow";
10 | import {useSoundLayerValue} from "../../data/SoundLayer";
11 |
12 | function Body({spotify}) {
13 | const [{current_playlist, tracks, track}] = useDataLayerValue();
14 | const [{playing, volume}, soundDispatch] = useSoundLayerValue();
15 |
16 | const startPlaying = () => {
17 | soundDispatch({
18 | type: "SET_PLAYING",
19 | playing: true
20 | });
21 | soundDispatch({
22 | type: "SET_VOLUME",
23 | volume: volume / 100
24 | });
25 | };
26 |
27 | const stopPlaying = () => {
28 | soundDispatch({
29 | type: "SET_PLAYING",
30 | playing: false
31 | });
32 | };
33 |
34 | return (
35 |
62 | )
63 | }
64 |
65 | export default Body
--------------------------------------------------------------------------------
/src/player/body/partials/Header.css:
--------------------------------------------------------------------------------
1 | .header {
2 | display: flex;
3 | justify-content: space-between;
4 | margin-bottom: 30px;
5 | }
6 |
7 | .header__left {
8 | flex: 0.5;
9 | background-color: white;
10 | color: gray;
11 | border-radius: 30px;
12 | padding: 10px;
13 | display: flex;
14 | align-items: center;
15 | }
16 |
17 | .header__left > input {
18 | border: none;
19 | width: 100%;
20 | }
21 |
22 | .header__right {
23 | display: flex;
24 | align-items: center;
25 | }
26 |
27 | .header__right > h4 {
28 | margin-left: 10px;
29 | }
--------------------------------------------------------------------------------
/src/player/body/partials/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './Header.css'
3 | import SearchIcon from '@material-ui/icons/Search';
4 | import {Avatar} from "@material-ui/core";
5 | import {useDataLayerValue} from "../../../data/DataLayer";
6 |
7 | function Header({ spotify }) {
8 | const [{user}] = useDataLayerValue();
9 |
10 |
11 | return (
12 |
13 |
14 |
15 |
18 |
19 |
20 |
21 |
{ user?.display_name }
22 |
23 |
24 | )
25 | }
26 |
27 | export default Header
--------------------------------------------------------------------------------
/src/player/body/partials/SongRow.css:
--------------------------------------------------------------------------------
1 | .songRow {
2 | margin-left: 20px;
3 | padding: 20px;
4 | display: fleX;
5 | align-items: center;
6 | color:white;
7 | }
8 |
9 | .songRow:hover {
10 | cursor: pointer;
11 | background-color: black;
12 | opacity: 0.8
13 | }
14 |
15 | .songRow__info {
16 | margin-left: 20px
17 | }
18 |
19 | .songRow__info > h1 {
20 | font-size: 16px;
21 | }
22 |
23 | .songRow__info > p {
24 | font-size: 14px;
25 | margin-top: 3px;
26 | color: grey;
27 | }
28 |
29 | .songRow__album {
30 | height: 40px;
31 | width: 40px;
32 | }
--------------------------------------------------------------------------------
/src/player/body/partials/SongRow.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './SongRow.css'
3 | import {useDataLayerValue} from '../../../data/DataLayer';
4 | import {useSoundLayerValue} from '../../../data/SoundLayer';
5 |
6 | function SongRow({track}) {
7 | const [{}, dispatch] = useDataLayerValue();
8 | const [{playing, repeat}, soundDispatch] = useSoundLayerValue();
9 |
10 | const changeTrack = (e, track) => {
11 | dispatch({
12 | type: 'SET_TRACK',
13 | track: track
14 | });
15 |
16 | let wasPlaying = playing;
17 | soundDispatch({
18 | type: 'SET_PLAYING',
19 | playing: false,
20 | });
21 |
22 | let audio = new Audio(track.preview_url);
23 | audio.loop = repeat;
24 | soundDispatch({
25 | type: 'SET_AUDIO',
26 | audio: audio
27 | });
28 |
29 | if(wasPlaying) {
30 | soundDispatch({
31 | type: 'SET_PLAYING',
32 | playing: true,
33 | });
34 | }
35 |
36 | document.title = `${track.name} · ${track.artists.map((artist) => artist.name).join(', ')}`
37 | };
38 | return (
39 | changeTrack(e, track)}>
40 |
41 |
42 |
{track.name}
43 |
44 | {track.artists.map((artist) => artist.name).join(', ')}
45 |
46 |
47 | {track.album.name}
48 |
49 |
50 |
51 | )
52 | }
53 |
54 | export default SongRow
--------------------------------------------------------------------------------
/src/player/footer/Footer.css:
--------------------------------------------------------------------------------
1 | .footer {
2 | position: fixed;
3 | display: flex;
4 | justify-content: space-between;
5 | bottom: 0;
6 | height: 65px;
7 | width: 100%;
8 | background-color: #282828;
9 | padding: 20px;
10 | }
11 |
12 | .footer__left {
13 | flex: 0.3;
14 | display: flex;
15 | align-items: center;
16 | color:white;
17 | width: 300px;
18 | }
19 |
20 | .footer__albumLogo {
21 | height: 60px;
22 | width: 60px;
23 | object-fit: contain;
24 | margin-right: 20px;
25 | }
26 |
27 | .footer__center {
28 | flex: 0.4;
29 | padding: 0 100px;
30 | color: white;
31 | display: flex;
32 | align-items: center;
33 | justify-content: space-between;
34 | max-width: 300px;
35 | }
36 |
37 | .footer__right {
38 | flex: 0.3;
39 | display: flex;
40 | align-items: center;
41 | justify-content: space-between;
42 | color: white;
43 | }
44 |
45 | .footer__right * .MuiSlider-root {
46 | color: #1ed15e;
47 | width: 90%;
48 | }
49 |
50 | .footer__green {
51 | color: #1ed15e;
52 | }
53 |
54 | .footer__songInfo > h4{
55 | margin-bottom: 5px;
56 | }
57 |
58 | .footer_songInfo > p {
59 | font-size: 12px;
60 | }
61 |
62 | .footer__icon:hover,
63 | .footer__green:hover {
64 | transition: transform 0.2s ease-in-out;
65 | transform: scale(1.2) !important;
66 | }
67 |
--------------------------------------------------------------------------------
/src/player/footer/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './Footer.css'
3 | import PlayCircleOutlineIcon from '@material-ui/icons/PlayCircleOutline';
4 | import SkipPreviousIcon from '@material-ui/icons/SkipPrevious';
5 | import SkipNextIcon from '@material-ui/icons/SkipNext';
6 | import ShuffleIcon from '@material-ui/icons/Shuffle';
7 | import RepeatIcon from '@material-ui/icons/Repeat';
8 | import {Grid, Slider} from '@material-ui/core';
9 | import PlaylistPlayIcon from '@material-ui/icons/PlaylistPlay';
10 | import VolumeDownIcon from '@material-ui/icons/VolumeDown';
11 | import PauseCircleOutlineIcon from '@material-ui/icons/PauseCircleOutline';
12 | import {useDataLayerValue} from "../../data/DataLayer";
13 | import {useSoundLayerValue} from "../../data/SoundLayer";
14 |
15 | function Footer() {
16 | const [{track, tracks}, dispatch] = useDataLayerValue();
17 | const [{audio, playing, volume, repeat, shuffle}, soundDispatch] = useSoundLayerValue();
18 |
19 |
20 | const startPlaying = () => {
21 | soundDispatch({
22 | type: "SET_PLAYING",
23 | playing: true
24 | });
25 | soundDispatch({
26 | type: "SET_VOLUME",
27 | volume: volume / 100
28 | });
29 | };
30 |
31 | const stopPlaying = () => {
32 | soundDispatch({
33 | type: "SET_PLAYING",
34 | playing: false
35 | });
36 | };
37 |
38 | const setRepeat = () => {
39 | if(!repeat && shuffle) {
40 | setShuffle();
41 | }
42 | soundDispatch({
43 | type: "SET_REPEAT",
44 | repeat: !repeat
45 | });
46 | };
47 |
48 | const setShuffle = () => {
49 | if(!shuffle && repeat) {
50 | setRepeat();
51 | }
52 | soundDispatch({
53 | type: "SET_SHUFFLE",
54 | shuffle: !shuffle
55 | });
56 | };
57 |
58 | const handleChange = (event, value) => {
59 | soundDispatch({
60 | type: "SET_VOLUME",
61 | volume: value / 100
62 | });
63 | };
64 |
65 | if(audio) {
66 | audio.onended = () => {
67 | if(shuffle) {
68 | while(true) {
69 | let randomTrackNumber = Math.floor((Math.random() * tracks.items.length));
70 | let randomTrack = tracks.items[randomTrackNumber].track;
71 | if(track !== randomTrack) {
72 | dispatch({
73 | type: 'SET_TRACK',
74 | track: randomTrack
75 | });
76 |
77 | let wasPlaying = playing;
78 | soundDispatch({
79 | type: 'SET_PLAYING',
80 | playing: false,
81 | });
82 |
83 | let audio = new Audio(randomTrack.preview_url);
84 | audio.loop = repeat;
85 | soundDispatch({
86 | type: 'SET_AUDIO',
87 | audio: audio
88 | });
89 |
90 | if(wasPlaying) {
91 | soundDispatch({
92 | type: 'SET_PLAYING',
93 | playing: true,
94 | });
95 | }
96 |
97 | document.title = `${randomTrack.name} · ${randomTrack.artists.map((artist) => artist.name).join(', ')}`
98 | break
99 | }
100 | }
101 | }
102 | if(!shuffle && !repeat) {
103 | soundDispatch({
104 | type: 'SET_PLAYING',
105 | playing: false,
106 | });
107 | }
108 | }
109 | }
110 |
111 | return (
112 |
113 |
114 |
115 |
116 |
{track ? track.name : 'No song selected'}
117 |
{track ? track.artists.map((artist) => artist.name).join(", ") : null}
118 |
119 |
120 |
121 |
122 |
123 | {playing ?
:
125 |
}
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
146 |
147 |
148 |
149 |
150 | )
151 | }
152 |
153 | export default Footer
--------------------------------------------------------------------------------
/src/player/sidebar/Sidebar.css:
--------------------------------------------------------------------------------
1 | .sidebar {
2 | height: 100vh;
3 | flex: 0.2;
4 | color: #fff;
5 | min-width: 230px;
6 | padding-left: 10px;
7 | padding-right: 10px;
8 | background-color: #040404;
9 | }
10 |
11 | .sidebar_logo {
12 | height: 70px;
13 | padding: 10px;
14 | margin-right: auto;
15 | }
--------------------------------------------------------------------------------
/src/player/sidebar/Sidebar.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './Sidebar.css'
3 | import SidebarOption from './partials/SidebarOption';
4 | import HomeIcon from '@material-ui/icons/Home';
5 | import SearchIcon from '@material-ui/icons/Search';
6 | import LibraryMusicIcon from '@material-ui/icons/LibraryMusic';
7 | import {useDataLayerValue} from "../../data/DataLayer";
8 |
9 | function Sidebar({spotify}) {
10 | const [{playlists}] = useDataLayerValue();
11 |
12 | return (
13 |
14 |
16 |
17 |
18 |
19 |
20 |
21 | PLAYLISTS
22 |
23 |
24 | {playlists?.items?.map((playlist) => {
25 | return
26 | })}
27 |
28 | )
29 | }
30 |
31 | export default Sidebar
--------------------------------------------------------------------------------
/src/player/sidebar/partials/SidebarOption.css:
--------------------------------------------------------------------------------
1 | .sidebarOption {
2 | display: flex;
3 | align-items: center;
4 | color: grey;
5 | height: 40px;
6 | cursor: pointer;
7 | transition: 200ms color ease-in;
8 | }
9 |
10 | .sidebar > hr {
11 | border: 1px solid #282828;
12 | width: 90%;
13 | margin: 10px auto;
14 | }
15 |
16 | .sidebarOption:hover {
17 | color: white;
18 | }
19 |
20 | .sidebarOption__icon {
21 | padding-left: 10px;
22 | padding-right: 10px;
23 | }
24 |
25 | .sidebar__title {
26 | margin-left: 10px;
27 | padding: 5px;
28 | font-size: 12px;
29 | }
30 |
31 | .sidebarOption > p {
32 | margin-top: 10px;
33 | margin-left: 10px;
34 | font-size: 14px;
35 | }
--------------------------------------------------------------------------------
/src/player/sidebar/partials/SidebarOption.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './SidebarOption.css'
3 | import {useDataLayerValue} from '../../../data/DataLayer';
4 |
5 | function SidebarOption({spotify, title, id, Icon}) {
6 | const [{}, dispatch] = useDataLayerValue();
7 |
8 | const changePlaylist = (id, e) => {
9 | dispatch({
10 | type: 'SET_CURRENT_PLAYLIST',
11 | id: id
12 | });
13 |
14 | spotify.getPlaylistTracks(id).then((response) => {
15 | dispatch({
16 | type: 'SET_TRACKS',
17 | tracks: response
18 | })
19 | });
20 | }
21 |
22 | return (
23 |
24 | {Icon &&
}
25 | {Icon ?
{title} :
changePlaylist(id, e)}>{title}
}
26 |
27 | )
28 | }
29 |
30 | export default SidebarOption
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' },
105 | })
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready
134 | .then(registration => {
135 | registration.unregister();
136 | })
137 | .catch(error => {
138 | console.error(error.message);
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/spotify.js:
--------------------------------------------------------------------------------
1 | //https://developer.spotify.com/documentation/web-playback-sdk/quick-start/
2 |
3 | export const authEndpoint = 'https://accounts.spotify.com/authorize';
4 |
5 | const redirectUrl = window.location.origin + '/'; //where are you running your app (local react by default is http://localhost:3000/
6 | const clientId = 'd3384f96a6054701a7cf023eb714c440'; // clintId you can get at https://developer.spotify.com/dashboard
7 |
8 | /**
9 | * You can read more about Spotify scopes at https://developer.spotify.com/documentation/general/guides/scopes/
10 | */
11 | const scopes = [
12 | 'user-read-currently-playing',
13 | 'user-read-recently-played',
14 | 'user-read-playback-state',
15 | 'user-top-read',
16 | 'user-modify-playback-state'
17 | ];
18 |
19 | export const loginUrl = `${authEndpoint}?client_id=${clientId}&redirect_uri=${redirectUrl}&scopes=${scopes.join('%20')}&response_type=token&show_dialog=true`;
20 |
21 | export const getAccessTokenFromUrl = () => {
22 | return window.location.hash
23 | .substring(1)
24 | .split('&')
25 | .reduce((initial, item) => {
26 | let parts = item.split('=');
27 | initial[parts[0]] = decodeURIComponent(parts[1]);
28 | return initial
29 | }, {});
30 | };
--------------------------------------------------------------------------------