├── README.md ├── auth-api ├── .env-example ├── .gitignore ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── procfile └── server.js ├── my-app ├── .editorconfig ├── .env-example ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── package.json ├── public │ ├── _redirects │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.js │ ├── assets │ │ ├── default-image.jpg │ │ ├── spotify-clone-app-logo-white.png │ │ └── spotify-clone-app-logo.png │ ├── components │ │ ├── Body │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Header │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── HomeItems │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Player │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Sidebar │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── SpotifyButton │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── UserRecentlyPlayed │ │ │ ├── index.js │ │ │ └── styles.css │ │ └── UserTopArtists │ │ │ ├── index.js │ │ │ └── styles.css │ ├── global.css │ ├── index.js │ ├── pages │ │ ├── Album │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Artist │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Categories │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Home │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Liked │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Login │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Playlist │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Profile │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Recently │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── Search │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── User │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── UserAlbums │ │ │ ├── index.js │ │ │ └── styles.css │ │ ├── UserArtists │ │ │ ├── index.js │ │ │ └── styles.css │ │ └── UserPlaylists │ │ │ ├── index.js │ │ │ └── styles.css │ ├── routes.js │ ├── services │ │ └── api.js │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ ├── player │ │ │ ├── actions.js │ │ │ └── reducer.js │ │ │ └── rootReducer.js │ └── utils │ │ ├── getHashParams.js │ │ └── millisToMinutesAndSeconds.js └── yarn.lock ├── spotify-clone-app-logo.png └── spotify-clone-app-screenshots.jpg /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | spotify-clone-app 4 |
5 |
6 | Spotify clone app 7 |

8 | 9 |

Um web app clone do spotify feito em ReactJS, utiliza a API do Spotify para obtenção e manipulação de dados como playlists, músicas e artistas favoritas do usuário 10 |

Aplicação rodando no Netlify 11 |

12 | 13 | # 📋 Índice 14 | 15 | - [Telas](#-Telas) 16 | - [Sobre o projeto](#-Sobre-o-projeto) 17 | - [Funcionalidades](#-Funcionalidades) 18 | - [Tecnologias utilizadas](#-Tecnologias-utilizadas) 19 | - [Rodando o projeto](#-Rodando-o-projeto) 20 | - [Pré-requisitos](#-Pré-requisitos) 21 | - [Rodando a auth-api](#-Rodando-a-auth-api) 22 | - [Rodando o front-end](#-Rodando-o-front-end) 23 | 24 | ## 🎨 Telas 25 | 26 | spotify-clone-app 27 | 28 | ## 📃 Sobre o projeto 29 | 30 | Um clone do spotify com algumas modificações visuais pequenas que em minha opinião melhoram um pouco a usabilidade. 31 | 32 | Desenvolvido para práticar ReactJS e API Rest 33 | 34 | Este projeto faz parte do meu potfólio pessoal, qualquer feedback sobre estrutura, código ou funcionalidades que podem melhorar o projeto serão bem vindos. 35 | 36 | Sinta-se livre para dar um fork, ou enviar um pull request, você pode usar este projeto para estudar ou fazer melhorias! 37 | 38 | ### Funcionalidades 39 | 40 | - Consultar as ultimas músicas escutadas por você 41 | - Consultar as músicas e artistas mais escutadas por você em todos os tempos ou nos últimos 6 meses ou 4 semanas 42 | - Consultar seus artistas favoritos 43 | - Realizar buscas por artistas, álbuns ou playlists 44 | - Consultar as músicas que você curtiu 45 | - Consultar suas playlists 46 | - Consultar seus artistas e álbuns salvos 47 | - Adicionar ou remover músicas, artistas, playlists, e álbuns da sua biblioteca 48 | 49 | ## 🛠 Tecnologias utilizadas 50 | 51 | - ⚛ **React** - Single page application 52 | - ⚛ **React Router** - Controle de rotas 53 | - ⚛ **Redux** - Controle de estado da track atual 54 | - ⚛ **React redux** - Controle de estado da track atual 55 | - 🎵 **React audio** player - Player de áudio 56 | - 🤙 **React icons** - Ícones da aplicação 57 | - 📡 **Axios** - Comunicação com a API do Spotify 58 | 59 | ## 🚀 Rodando o projeto 60 | 61 | A aplicação é dividida em duas partes, my-app, que é o front-end e auth-api, que é a comunicação com a API do Spotify no login, para conseguir logar é necessário que a auth-api esteja sendo executada. 62 | 63 | ### Pré-requisitos 64 | 65 | - Git 66 | - NodeJS 67 | - Yarn 68 | - Uma conta no Spotify 69 | 70 | ### 💻 Rodando a auth-api 71 | 72 | Clone o repositório 73 | 74 | ```bash 75 | 76 | # Clona o repositório 77 | git clone https://github.com/thiagosprestes/Spotify-clone-app-react.git 78 | 79 | ``` 80 | 81 | Navegue até a pasta do projeto clonado e execute os comandos abaixo 82 | 83 | ```bash 84 | 85 | # Entra na pasta da auth-api 86 | cd auth-api 87 | 88 | # Instala as dependências 89 | npm install 90 | 91 | ``` 92 | Após instalar as dependências 93 | 94 | ```bash 95 | 96 | # Inicia a auth-api 97 | npm run dev 98 | 99 | ``` 100 | 101 | ### 🖥 Rodando o front-end 102 | 103 | Caso já tenha clonado o repositório basta pular a primeira etapa 104 | 105 | ```bash 106 | 107 | # Clona o repositório 108 | git clone https://github.com/thiagosprestes/Spotify-clone-app-react.git 109 | 110 | ``` 111 | 112 | Navegue até a pasta do projeto clonado e execute os comandos abaixo 113 | 114 | ```bash 115 | 116 | # Entra na pasta do front-end 117 | cd my-app 118 | 119 | # Instala as dependências 120 | yarn 121 | 122 | ``` 123 | Após concluir a instalação das dependências, ainda no terminal da pasta do front-end execute o comando abaixo 124 | 125 | ```bash 126 | 127 | # Inicia a aplicação 128 | yarn start 129 | 130 | # Após isso a aplicação pode ser utilizada acessando o endereço http://localhost:3000 131 | 132 | ``` 133 | -------------------------------------------------------------------------------- /auth-api/.env-example: -------------------------------------------------------------------------------- 1 | # Spotify client ID 2 | CLIENT_ID= 3 | 4 | # Spotify client secret 5 | CLIENT_SECRET= 6 | 7 | # Auth api redirect uri 8 | REDIRECT_URI= 9 | 10 | # Frontend redirect if auth success 11 | APP_REDIRECT= -------------------------------------------------------------------------------- /auth-api/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env -------------------------------------------------------------------------------- /auth-api/README.md: -------------------------------------------------------------------------------- 1 | # Spotify Accounts Authentication Examples 2 | 3 | This project contains basic demos showing the different OAuth 2.0 flows for [authenticating against the Spotify Web API](https://developer.spotify.com/web-api/authorization-guide/). 4 | 5 | These examples cover: 6 | 7 | * Authorization Code flow 8 | * Client Credentials flow 9 | * Implicit Grant flow 10 | 11 | ## Installation 12 | 13 | These examples run on Node.js. On [its website](http://www.nodejs.org/download/) you can find instructions on how to install it. You can also follow [this gist](https://gist.github.com/isaacs/579814) for a quick and easy way to install Node.js and npm. 14 | 15 | Once installed, clone the repository and install its dependencies running: 16 | 17 | $ npm install 18 | 19 | ### Using your own credentials 20 | You will need to register your app and get your own credentials from the Spotify for Developers Dashboard. 21 | 22 | To do so, go to [your Spotify for Developers Dashboard](https://beta.developer.spotify.com/dashboard) and create your application. For the examples, we registered these Redirect URIs: 23 | 24 | * http://localhost:8888 (needed for the implicit grant flow) 25 | * http://localhost:8888/callback 26 | 27 | Once you have created your app, replace the `client_id`, `redirect_uri` and `client_secret` in the examples with the ones you get from My Applications. 28 | 29 | ## Running the examples 30 | In order to run the different examples, open the folder with the name of the flow you want to try out, and run its `app.js` file. For instance, to run the Authorization Code example do: 31 | 32 | $ cd authorization_code 33 | $ node app.js 34 | 35 | Then, open `http://localhost:8888` in a browser. 36 | -------------------------------------------------------------------------------- /auth-api/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an example of a basic node.js script that performs 3 | * the Authorization Code oAuth2 flow to authenticate against 4 | * the Spotify Accounts. 5 | * 6 | * For more information, read 7 | * https://developer.spotify.com/web-api/authorization-guide/#authorization_code_flow 8 | */ 9 | require('dotenv/config'); 10 | 11 | var express = require('express'); // Express web server framework 12 | var request = require('request'); // "Request" library 13 | var cors = require('cors'); 14 | var querystring = require('querystring'); 15 | var cookieParser = require('cookie-parser'); 16 | 17 | var client_id = process.env.CLIENT_ID; // Your client id 18 | var client_secret = process.env.CLIENT_SECRET; // Your secret 19 | var redirect_uri = process.env.REDIRECT_URI || 'http://localhost:8888/callback'; // Your redirect uri 20 | 21 | /** 22 | * Generates a random string containing numbers and letters 23 | * @param {number} length The length of the string 24 | * @return {string} The generated string 25 | */ 26 | var generateRandomString = function(length) { 27 | var text = ''; 28 | var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 29 | 30 | for (var i = 0; i < length; i++) { 31 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 32 | } 33 | return text; 34 | }; 35 | 36 | var stateKey = 'spotify_auth_state'; 37 | 38 | var app = express(); 39 | 40 | app.use(express.static(__dirname + '/public')) 41 | .use(cors()) 42 | .use(cookieParser()); 43 | 44 | app.get('/login', function(req, res) { 45 | 46 | var state = generateRandomString(16); 47 | res.cookie(stateKey, state); 48 | 49 | // your application requests authorization 50 | var scope = 'user-read-private user-read-email playlist-read-private user-library-read user-library-modify user-top-read playlist-read-collaborative playlist-modify-public playlist-modify-private user-follow-read user-read-playback-state user-read-currently-playing user-modify-playback-state user-read-recently-played user-follow-modify'; 51 | res.redirect('https://accounts.spotify.com/authorize?' + 52 | querystring.stringify({ 53 | response_type: 'code', 54 | client_id: client_id, 55 | scope: scope, 56 | redirect_uri: redirect_uri, 57 | state: state 58 | })); 59 | }); 60 | 61 | app.get('/callback', function(req, res) { 62 | 63 | // your application requests refresh and access tokens 64 | // after checking the state parameter 65 | 66 | var code = req.query.code || null; 67 | var state = req.query.state || null; 68 | var storedState = req.cookies ? req.cookies[stateKey] : null; 69 | 70 | if (state === null || state !== storedState) { 71 | res.redirect('/#' + 72 | querystring.stringify({ 73 | error: 'state_mismatch' 74 | })); 75 | } else { 76 | res.clearCookie(stateKey); 77 | var authOptions = { 78 | url: 'https://accounts.spotify.com/api/token', 79 | form: { 80 | code: code, 81 | redirect_uri: redirect_uri, 82 | grant_type: 'authorization_code' 83 | }, 84 | headers: { 85 | 'Authorization': 'Basic ' + (new Buffer(client_id + ':' + client_secret).toString('base64')) 86 | }, 87 | json: true 88 | }; 89 | 90 | request.post(authOptions, function(error, response, body) { 91 | if (!error && response.statusCode === 200) { 92 | 93 | var access_token = body.access_token, 94 | refresh_token = body.refresh_token; 95 | 96 | var options = { 97 | url: 'https://api.spotify.com/v1/me', 98 | headers: { 'Authorization': 'Bearer ' + access_token }, 99 | json: true 100 | }; 101 | 102 | // use the access token to access the Spotify Web API 103 | request.get(options, function(error, response, body) { 104 | console.log(body); 105 | }); 106 | 107 | // we can also pass the token to the browser to make requests from there 108 | res.redirect(`${process.env.APP_REDIRECT || 'http://localhost:3000'}/#` + 109 | querystring.stringify({ 110 | access_token: access_token, 111 | refresh_token: refresh_token 112 | })); 113 | } else { 114 | res.redirect('/#' + 115 | querystring.stringify({ 116 | error: 'invalid_token' 117 | })); 118 | } 119 | }); 120 | } 121 | }); 122 | 123 | app.get('/refresh_token', function(req, res) { 124 | 125 | // requesting access token from refresh token 126 | var refresh_token = req.query.refresh_token; 127 | var authOptions = { 128 | url: 'https://accounts.spotify.com/api/token', 129 | headers: { 'Authorization': 'Basic ' + (new Buffer(client_id + ':' + client_secret).toString('base64')) }, 130 | form: { 131 | grant_type: 'refresh_token', 132 | refresh_token: refresh_token 133 | }, 134 | json: true 135 | }; 136 | 137 | request.post(authOptions, function(error, response, body) { 138 | if (!error && response.statusCode === 200) { 139 | var access_token = body.access_token; 140 | res.send({ 141 | 'access_token': access_token 142 | }); 143 | } 144 | }); 145 | }); 146 | 147 | module.exports = app; -------------------------------------------------------------------------------- /auth-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Spotify", 3 | "name": "web-api-auth-examples", 4 | "description": "Basic examples of the Spotify authorization flows through OAuth 2", 5 | "version": "0.0.2", 6 | "scripts": { 7 | "dev": "nodemon server.js", 8 | "start": "node server.js" 9 | }, 10 | "dependencies": { 11 | "cookie-parser": "1.3.2", 12 | "cors": "^2.8.4", 13 | "dotenv": "^8.2.0", 14 | "express": "~4.16.0", 15 | "querystring": "~0.2.0", 16 | "request": "~2.83.0" 17 | }, 18 | "devDependencies": { 19 | "nodemon": "^2.0.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /auth-api/procfile: -------------------------------------------------------------------------------- 1 | web: npm run start -------------------------------------------------------------------------------- /auth-api/server.js: -------------------------------------------------------------------------------- 1 | const app = require('./index'); 2 | 3 | app.listen(process.env.PORT || 8888); -------------------------------------------------------------------------------- /my-app/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 4 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /my-app/.env-example: -------------------------------------------------------------------------------- 1 | # Auth api url for authenticate user and redirect to app home 2 | REACT_APP_AUTH_API_URL= -------------------------------------------------------------------------------- /my-app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | }, 6 | extends: [ 7 | 'plugin:react/recommended', 8 | 'airbnb', 9 | 'prettier', 10 | 'prettier/react', 11 | ], 12 | globals: { 13 | Atomics: 'readonly', 14 | SharedArrayBuffer: 'readonly', 15 | }, 16 | parserOptions: { 17 | ecmaFeatures: { 18 | jsx: true, 19 | }, 20 | ecmaVersion: 11, 21 | sourceType: 'module', 22 | }, 23 | plugins: [ 24 | 'react', 25 | 'prettier', 26 | 'react-hooks', 27 | ], 28 | rules: { 29 | 'prettier/prettier': 'error', 30 | 'react/jsx-filename-extension': [ 31 | 'warn', 32 | { extensions: ['.jsx', '.js'] }, 33 | ], 34 | 'react/state-in-constructor': 'off', 35 | 'react/static-property-placement': 'off', 36 | 'react/jsx-props-no-spreading': 'off', 37 | 'react/prop-types': 'off', 38 | 'no-param-reassign': 'off', 39 | 'no-console': 'off', 40 | 'import/prefer-default-export': 'off', 41 | 'react-hooks/rules-of-hooks': 'error', 42 | 'react-hooks/exhaustive-deps': 'warn', 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /my-app/.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 | 25 | .env 26 | -------------------------------------------------------------------------------- /my-app/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /my-app/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `yarn build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /my-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spotify-clone-app-in-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "axios": "^0.19.2", 10 | "lodash": "^4.17.15", 11 | "react": "^16.13.0", 12 | "react-dom": "^16.13.0", 13 | "react-h5-audio-player": "^3.0.2", 14 | "react-icons": "^3.9.0", 15 | "react-redux": "^7.2.0", 16 | "react-router-dom": "^5.1.2", 17 | "react-scripts": "3.4.0", 18 | "redux": "^4.0.5" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": "react-app" 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | }, 41 | "devDependencies": { 42 | "eslint": "^6.8.0", 43 | "eslint-config-airbnb": "^18.1.0", 44 | "eslint-config-prettier": "^6.11.0", 45 | "eslint-plugin-import": "^2.20.2", 46 | "eslint-plugin-jsx-a11y": "^6.2.3", 47 | "eslint-plugin-prettier": "^3.1.3", 48 | "eslint-plugin-react": "^7.20.0", 49 | "eslint-plugin-react-hooks": "^2.5.1", 50 | "prettier": "^2.0.5" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /my-app/public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /my-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagosprestes/Spotify-clone-app-react/26ad1dcb716a243a0fd20bb9cdd5c14068550688/my-app/public/favicon.ico -------------------------------------------------------------------------------- /my-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Spotify Clone App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /my-app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagosprestes/Spotify-clone-app-react/26ad1dcb716a243a0fd20bb9cdd5c14068550688/my-app/public/logo192.png -------------------------------------------------------------------------------- /my-app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagosprestes/Spotify-clone-app-react/26ad1dcb716a243a0fd20bb9cdd5c14068550688/my-app/public/logo512.png -------------------------------------------------------------------------------- /my-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /my-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /my-app/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './global.css'; 4 | 5 | import Routes from './routes'; 6 | 7 | function App() { 8 | return ; 9 | } 10 | 11 | export default App; 12 | -------------------------------------------------------------------------------- /my-app/src/assets/default-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagosprestes/Spotify-clone-app-react/26ad1dcb716a243a0fd20bb9cdd5c14068550688/my-app/src/assets/default-image.jpg -------------------------------------------------------------------------------- /my-app/src/assets/spotify-clone-app-logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagosprestes/Spotify-clone-app-react/26ad1dcb716a243a0fd20bb9cdd5c14068550688/my-app/src/assets/spotify-clone-app-logo-white.png -------------------------------------------------------------------------------- /my-app/src/assets/spotify-clone-app-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagosprestes/Spotify-clone-app-react/26ad1dcb716a243a0fd20bb9cdd5c14068550688/my-app/src/assets/spotify-clone-app-logo.png -------------------------------------------------------------------------------- /my-app/src/components/Body/index.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | 3 | import { Provider } from 'react-redux'; 4 | 5 | import store from '../../store'; 6 | 7 | import './styles.css'; 8 | 9 | import Sidebar from '../Sidebar'; 10 | import Header from '../Header'; 11 | import Player from '../Player'; 12 | 13 | function Body({ children }) { 14 | const [showSidebar, setShowSidebar] = useState(false); 15 | 16 | const toggleSidebar = useCallback( 17 | () => setShowSidebar((value) => !value), 18 | [] 19 | ); 20 | 21 | return ( 22 | 23 |
24 | 27 |
34 |
38 | {children} 39 |
40 |
41 | 42 |
43 |
44 |
45 | ); 46 | } 47 | 48 | export default Body; 49 | -------------------------------------------------------------------------------- /my-app/src/components/Body/styles.css: -------------------------------------------------------------------------------- 1 | #app { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: flex-start; 5 | height: 100%; 6 | } 7 | 8 | #app main { 9 | margin-left: 230px; 10 | width: 100%; 11 | background-color: #121212; 12 | color: #fff; 13 | min-height: 100%; 14 | } 15 | 16 | #app main #header { 17 | padding: 20px 20px; 18 | margin-top: 0; 19 | } 20 | 21 | #app main > div { 22 | margin-top: 55px; 23 | margin-bottom: 50px; 24 | } 25 | 26 | #app footer { 27 | position: fixed; 28 | bottom: 0; 29 | width: 100%; 30 | } 31 | 32 | .loading { 33 | display: flex; 34 | align-items: center; 35 | justify-content: center; 36 | height: 100vh; 37 | } 38 | 39 | 40 | @media (max-width: 810px) { 41 | #app aside { 42 | position: absolute; 43 | transition: .2s; 44 | margin-top: 54px; 45 | } 46 | 47 | #app main { 48 | margin: 0; 49 | transition: .2s; 50 | } 51 | } 52 | 53 | @media (max-width: 800px) and (max-height: 450px) { 54 | #app main { 55 | width: 100%; 56 | position: absolute !important; 57 | } 58 | } -------------------------------------------------------------------------------- /my-app/src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from 'react'; 2 | 3 | import { Link, useHistory } from 'react-router-dom'; 4 | 5 | import './styles.css'; 6 | 7 | import { GiHamburgerMenu } from 'react-icons/gi'; 8 | 9 | import api from '../../services/api'; 10 | 11 | function Header({ sidebarState }) { 12 | const [user, setUser] = useState(''); 13 | 14 | const [showDropdown, setShowDropdown] = useState(false); 15 | 16 | const history = useHistory(); 17 | 18 | async function loadUser() { 19 | const response = await api.get('/me'); 20 | 21 | setUser(response.data.display_name); 22 | 23 | localStorage.setItem('user', response.data.display_name); 24 | } 25 | 26 | useEffect(() => { 27 | loadUser(); 28 | }, []); 29 | 30 | function logout() { 31 | localStorage.removeItem('user'); 32 | history.push('/login'); 33 | } 34 | 35 | const dropdownMenu = useCallback( 36 | () => setShowDropdown((value) => !value), 37 | [] 38 | ); 39 | 40 | return ( 41 | 68 | ); 69 | } 70 | 71 | export default Header; 72 | -------------------------------------------------------------------------------- /my-app/src/components/Header/styles.css: -------------------------------------------------------------------------------- 1 | #header { 2 | background-color: rgba(0, 0, 0, 0.9); 3 | color: #FFF; 4 | display: flex; 5 | justify-content: flex-end; 6 | padding: 20px; 7 | position: fixed; 8 | top: 0; 9 | left: 230px; 10 | right: 0; 11 | height: 54px; 12 | z-index: 1000; 13 | } 14 | 15 | #header .sidebar-toggle { 16 | display: none; 17 | } 18 | 19 | #header .user-menu .user { 20 | text-align: right; 21 | } 22 | 23 | #header .user-menu .user-content { 24 | display: none; 25 | background-color: #282828; 26 | color: #b3b3b3; 27 | font-size: 14px; 28 | min-width: 160px; 29 | text-align: left; 30 | } 31 | 32 | #header .user-menu:hover .user-content { 33 | display: block; 34 | } 35 | 36 | #header .user-menu .user-content ul { 37 | list-style: none; 38 | } 39 | 40 | #header .user-menu .user-content ul li { 41 | padding: 10px 20px; 42 | } 43 | 44 | #header .user-menu .user-content ul li:hover { 45 | color: #fff; 46 | background-color: #333; 47 | cursor: pointer; 48 | } 49 | 50 | #header .user-menu .user-content ul li + li { 51 | border-top: 1px solid #ccc; 52 | } 53 | 54 | #header .user { 55 | font-weight: bold; 56 | font-size: 14px; 57 | } 58 | 59 | #header .user:hover { 60 | cursor: pointer; 61 | } 62 | 63 | #header .logout { 64 | background: transparent; 65 | color: #FFF; 66 | border: 4px solid #fff; 67 | padding: 10px 20px; 68 | } 69 | 70 | @media (max-width: 810px) { 71 | #header { 72 | left: 0; 73 | justify-content: space-between; 74 | background-color: #000; 75 | } 76 | 77 | #header .sidebar-toggle { 78 | display: block; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /my-app/src/components/HomeItems/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Link } from 'react-router-dom'; 4 | 5 | import './styles.css'; 6 | 7 | function homeItems({ itemType, itemTitle, itemData, itemDataCategories }) { 8 | return ( 9 | <> 10 |

{itemTitle}

11 |
12 | {itemDataCategories !== undefined && 13 | itemType === 'category' && 14 | itemDataCategories.map((data) => ( 15 |
16 | 17 |
23 | 24 | 25 | {data.name} 26 | 27 |
28 | ))} 29 | {itemData !== undefined && 30 | itemType === 'album' && 31 | itemData.map((data) => ( 32 |
33 | 34 |
40 | {data.name} 41 | 42 | {data.artists !== undefined && ( 43 |
44 | {data.artists.map((artist) => ( 45 | 49 | {artist.name} 50 | 51 | ))} 52 |
53 | )} 54 |
55 | ))} 56 | 57 | {itemData !== undefined && 58 | itemType === 'playlist' && 59 | itemData.map((data) => ( 60 |
61 | 62 |
68 | {data.name} 69 | 70 | {data.artists !== undefined && ( 71 |
72 | {data.artists.map((artist) => ( 73 | 77 | {artist.name} 78 | 79 | ))} 80 |
81 | )} 82 |
83 | ))} 84 |
85 | 86 | ); 87 | } 88 | 89 | export default homeItems; 90 | -------------------------------------------------------------------------------- /my-app/src/components/HomeItems/styles.css: -------------------------------------------------------------------------------- 1 | .items { 2 | margin: 15px 0 30px 0; 3 | } 4 | 5 | .items .item-info { 6 | display: flex; 7 | flex-direction: column; 8 | } 9 | 10 | .items .item-info .item-cover { 11 | width: 100%; 12 | background-position: 50%; 13 | margin-bottom: 10px; 14 | } 15 | 16 | .items .item-info .item-name { 17 | font-weight: bold; 18 | font-size: 14px; 19 | margin: 8px 0; 20 | } 21 | 22 | .items .item-info .artists-name { 23 | font-size: 13px; 24 | margin-bottom: 20px; 25 | color: #CCC; 26 | } 27 | 28 | .items .item-info .artists-name a + a::before { 29 | content: ", "; 30 | } 31 | 32 | .items .item-info .artists-name a span:hover{ 33 | color: #fff; 34 | text-decoration: underline; 35 | } 36 | 37 | @media (max-width: 810px) { 38 | .items .item-info .item-cover { 39 | height: 9rem !important; 40 | width: 9rem !important; 41 | } 42 | } -------------------------------------------------------------------------------- /my-app/src/components/Player/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import { useSelector } from 'react-redux'; 4 | 5 | import { Link } from 'react-router-dom'; 6 | 7 | import './styles.css'; 8 | 9 | import { FaSpotify } from 'react-icons/fa'; 10 | 11 | import { IoMdCloseCircleOutline } from 'react-icons/io'; 12 | 13 | import AudioPlayer from 'react-h5-audio-player'; 14 | 15 | import 'react-h5-audio-player/lib/styles.css'; 16 | 17 | export default function Player() { 18 | const [togglePlayer, setTogglePlayer] = useState(false); 19 | 20 | const trackData = useSelector((state) => state.player.data); 21 | 22 | useEffect(() => { 23 | setTogglePlayer(true); 24 | }, [trackData]); 25 | 26 | return ( 27 |
31 | {trackData.length !== 0 && ( 32 | <> 33 |
34 |
setTogglePlayer(false)} 37 | > 38 | 39 |
40 | 41 |
47 | 48 |
49 | 50 | 51 | {trackData.name} 52 | 53 | 54 | {trackData.artists.map((artist) => ( 55 | 59 | 60 | {artist.name} 61 | 62 | 63 | ))} 64 |
65 |
66 |
67 | 75 |
76 | 96 | 97 | )} 98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /my-app/src/components/Player/styles.css: -------------------------------------------------------------------------------- 1 | #player { 2 | background-color: #282828; 3 | border-top: 1px solid #000; 4 | display: flex; 5 | flex-direction: row; 6 | align-items: center; 7 | color: #b3b3b3; 8 | justify-content: space-between; 9 | } 10 | 11 | #player .track-data { 12 | display: flex; 13 | align-items: center; 14 | } 15 | 16 | #player .track-data .track-cover { 17 | width: 3.5rem; 18 | height: 3.5rem; 19 | margin: 15px; 20 | } 21 | 22 | #player .track-data .track-info { 23 | display: flex; 24 | flex-direction: column; 25 | } 26 | 27 | #player .track-data .track-info .track-name { 28 | color: #FFF; 29 | font-size: 14px; 30 | margin-bottom: 5px; 31 | } 32 | 33 | #player .track-data .track-info .track-name:hover { 34 | text-decoration: underline; 35 | text-underline-offset: 4px; 36 | } 37 | 38 | #player .track-data .track-info .track-artist { 39 | color: #c3c3c3; 40 | font-size: 13px; 41 | } 42 | 43 | #player .track-data .track-info .track-artist + .track-artist { 44 | content: ", "; 45 | } 46 | 47 | #player .track-data .track-info .track-artist:hover { 48 | color: #fff; 49 | } 50 | 51 | #player .player-control { 52 | display: flex; 53 | flex-direction: column; 54 | justify-items: center; 55 | width: 30%; 56 | } 57 | 58 | #player .player-control .player-controls { 59 | display: flex; 60 | justify-content: center; 61 | align-items: center; 62 | margin-bottom: 20px; 63 | } 64 | 65 | #player .player-control .progress-bar { 66 | background-color: #404040; 67 | border-radius: 2px; 68 | display: -webkit-box; 69 | display: -ms-flexbox; 70 | display: flex; 71 | height: 4px; 72 | width: 100%; 73 | } 74 | 75 | #player .player-control .player-controls .play-pause { 76 | margin: 0 15px; 77 | transition: transform .1s; 78 | } 79 | 80 | #player .player-control .player-controls .play-pause:hover { 81 | color: #FFF; 82 | transform: scale(1.1); 83 | } 84 | 85 | #player .options { 86 | margin: 15px; 87 | display: flex; 88 | align-items: center; 89 | } 90 | 91 | #player .options .spotify { 92 | display: flex; 93 | align-items: center; 94 | background: transparent; 95 | color: #fff; 96 | border: 2px solid #FFF; 97 | transition: .2s; 98 | font-weight: bold; 99 | padding: 20px; 100 | border-radius: 500px; 101 | text-transform: uppercase; 102 | letter-spacing: 1px; 103 | font-size: 14px; 104 | line-height: 1; 105 | cursor: pointer; 106 | } 107 | 108 | #player .options .spotify:hover { 109 | transform: scale(1.06); 110 | background-color: #1DB954; 111 | border-color: #1DB954; 112 | color: #000; 113 | } 114 | 115 | #player .options .spotify svg { 116 | margin-right: 5px; 117 | } 118 | 119 | #player .track-slider { 120 | width: 50%; 121 | } 122 | 123 | #player .track-slider .rhap_container { 124 | background-color: transparent; 125 | box-shadow: none; 126 | } 127 | 128 | #player .track-slider .rhap_time { 129 | color: inherit; 130 | font-size: 14px; 131 | } 132 | 133 | #player .track-slider .rhap_progress-indicator { 134 | background: #fff; 135 | display: none; 136 | } 137 | 138 | #player .track-slider .rhap_progress-container:hover .rhap_progress-indicator { 139 | display: block; 140 | } 141 | 142 | #player .track-slider .rhap_progress-container:hover .rhap_progress-filled { 143 | background-color: #1DB954; 144 | } 145 | 146 | #player .options .spotify-icon { 147 | color: #FFF; 148 | display: none; 149 | } 150 | 151 | #player .options .spotify-icon:hover { 152 | color: #1DB954; 153 | } 154 | 155 | #player .track-data .close-player { 156 | margin-left: 15px; 157 | } 158 | 159 | #player .track-data .close-player:hover { 160 | cursor: pointer; 161 | } 162 | 163 | @media (max-width: 810px) { 164 | #player { 165 | flex-direction: column; 166 | } 167 | 168 | #player .track-data .track-info .track-artist { 169 | display: none; 170 | } 171 | 172 | #player .track-slider { 173 | width: 100% !important; 174 | margin-bottom: 0 !important; 175 | } 176 | 177 | #player .options { 178 | display: none; 179 | } 180 | 181 | #player .track-data .track-cover { 182 | display: none; 183 | } 184 | 185 | #player .track-data .track-info { 186 | margin: 15px 45px; 187 | width: 85%; 188 | } 189 | 190 | #player .options .spotify { 191 | display: none; 192 | } 193 | 194 | #player .track-data .close-player { 195 | position: absolute; 196 | left: 0; 197 | margin-left: 10px; 198 | } 199 | 200 | #player .track-data .track-info .track-name { 201 | font-weight: bold; 202 | } 203 | 204 | .rhap_stacked-reverse { 205 | flex-direction: row !important; 206 | } 207 | 208 | .rhap_additional-controls, .rhap_rewind-button, .rhap_forward-button, .rhap_volume-controls { 209 | display: none !important; 210 | } 211 | 212 | 213 | .rhap_stacked-reverse .rhap_controls-section { 214 | margin-bottom: 0 !important; 215 | position: absolute !important; 216 | top: 5px !important; 217 | right: 0 !important; 218 | } 219 | 220 | 221 | .rhap_container { 222 | padding: 0 !important; 223 | } 224 | 225 | .rhap_controls-section { 226 | flex: 0 !important; 227 | } 228 | 229 | 230 | .rhap_progress-container { 231 | height: 0 !important; 232 | margin: 0 !important; 233 | width: 100% !important; 234 | top: 0 !important; 235 | position: absolute !important; 236 | } 237 | 238 | .rhap_progress-bar { 239 | border-radius: 0px !important; 240 | } 241 | 242 | .rhap_time { 243 | display: none !important; 244 | } 245 | } -------------------------------------------------------------------------------- /my-app/src/components/Sidebar/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { NavLink } from 'react-router-dom'; 4 | 5 | import { 6 | MdHome, 7 | MdPerson, 8 | MdPlaylistPlay, 9 | MdMusicNote, 10 | MdAlbum, 11 | } from 'react-icons/md'; 12 | 13 | import { FiSearch, FiClock } from 'react-icons/fi'; 14 | 15 | import { FaHeart } from 'react-icons/fa'; 16 | 17 | import './styles.css'; 18 | 19 | import logo from '../../assets/spotify-clone-app-logo-white.png'; 20 | 21 | function Sidebar({ sidebarState }) { 22 | const menuItems = [ 23 | { 24 | label: 'Início', 25 | path: '/', 26 | icon: , 27 | }, 28 | { 29 | label: 'Buscar', 30 | path: '/search', 31 | icon: , 32 | }, 33 | { 34 | label: 'Perfil', 35 | path: '/profile', 36 | icon: , 37 | }, 38 | ]; 39 | 40 | const libraryItems = [ 41 | { 42 | label: 'Recentes', 43 | path: '/recently-played', 44 | icon: , 45 | }, 46 | { 47 | label: 'Músicas curtidas', 48 | path: '/collection/tracks', 49 | icon: , 50 | }, 51 | { 52 | label: 'Playlists', 53 | path: '/collection/playlists', 54 | icon: , 55 | }, 56 | { 57 | label: 'Artistas', 58 | path: '/collection/artists', 59 | icon: , 60 | }, 61 | { 62 | label: 'Álbuns', 63 | path: '/collection/albums', 64 | icon: , 65 | }, 66 | ]; 67 | 68 | return ( 69 | 105 | ); 106 | } 107 | 108 | export default Sidebar; 109 | -------------------------------------------------------------------------------- /my-app/src/components/Sidebar/styles.css: -------------------------------------------------------------------------------- 1 | #sidebar { 2 | background-color: #040404; 3 | width: 230px; 4 | padding-bottom: 20px; 5 | color: #b3b3b3; 6 | font-size: 14px; 7 | padding: 0 12px; 8 | position: fixed; 9 | height: 100%; 10 | } 11 | 12 | #sidebar img { 13 | padding: 22px 25px 15px 25px; 14 | width: 180px; 15 | } 16 | 17 | #sidebar ul { 18 | list-style: none; 19 | margin-bottom: 15px; 20 | } 21 | 22 | #sidebar .menu-item { 23 | color: #b3b3b3; 24 | font-weight: bold; 25 | font-size: 13px; 26 | transition: .2s; 27 | } 28 | 29 | #sidebar .menu-item .item-link { 30 | display: flex; 31 | flex-direction: row; 32 | align-items: center; 33 | padding: 5px 12px; 34 | } 35 | 36 | #sidebar .menu-item .active { 37 | color: #FFF; 38 | background-color: #282828; 39 | padding: 5px 12px; 40 | border-radius: 3px; 41 | } 42 | 43 | #sidebar .menu-item:hover { 44 | color: #fff; 45 | } 46 | 47 | #sidebar .menu-item .item-text { 48 | margin-left: 15px; 49 | } 50 | 51 | #sidebar .user-playlists span { 52 | letter-spacing: .10em; 53 | font-weight: bold; 54 | text-transform: uppercase; 55 | font-size: 12px; 56 | } 57 | 58 | #sidebar .user-playlists ul { 59 | list-style: none; 60 | font-weight: bold; 61 | font-size: 14px; 62 | margin: 15px 0; 63 | } 64 | 65 | #sidebar .user-playlists li:hover { 66 | color: #fff; 67 | } 68 | 69 | @media (max-width: 800px) { 70 | #sidebar { 71 | overflow-x: auto; 72 | } 73 | } -------------------------------------------------------------------------------- /my-app/src/components/SpotifyButton/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './styles.css'; 4 | 5 | import { FaSpotify } from 'react-icons/fa'; 6 | 7 | function SpotifyButton({ type, id }) { 8 | return ( 9 | 19 | ); 20 | } 21 | 22 | export default SpotifyButton; 23 | -------------------------------------------------------------------------------- /my-app/src/components/SpotifyButton/styles.css: -------------------------------------------------------------------------------- 1 | .spotify-link { 2 | margin: 15px 0; 3 | } 4 | 5 | .spotify-link a { 6 | display: inline-flex; 7 | align-items: center; 8 | border: 3px solid #fff; 9 | padding: 15px; 10 | border-radius: 50px; 11 | transition: .2s; 12 | font-weight: bold; 13 | font-size: 16px; 14 | color: #fff; 15 | } 16 | 17 | .spotify-link a svg { 18 | margin-right: 5px; 19 | } 20 | 21 | .spotify-link a:hover { 22 | background-color: #1DB954; 23 | border-color: #1DB954; 24 | color: #000; 25 | transform: scale(1.06); 26 | } -------------------------------------------------------------------------------- /my-app/src/components/UserRecentlyPlayed/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import { Link } from 'react-router-dom'; 4 | 5 | import './styles.css'; 6 | 7 | import { FaPlay } from 'react-icons/fa'; 8 | 9 | import { useSelector, useDispatch } from 'react-redux'; 10 | 11 | import api from '../../services/api'; 12 | 13 | import millisToMinutesAndSeconds from '../../utils/millisToMinutesAndSeconds'; 14 | 15 | import * as Player from '../../store/modules/player/actions'; 16 | 17 | function UserRecentlyPlayed() { 18 | const dispatch = useDispatch(); 19 | const [recentlyPlayed, setRecentlyPlayed] = useState([]); 20 | 21 | const trackData = useSelector((state) => state.player.data); 22 | 23 | async function handleLoad() { 24 | const response = await api.get('/me/player/recently-played?limit=10'); 25 | 26 | setRecentlyPlayed(response.data.items); 27 | } 28 | 29 | useEffect(() => { 30 | handleLoad(); 31 | }, []); 32 | 33 | return ( 34 |
35 |

Tocadas recentemente

36 | 37 | 38 | {recentlyPlayed.map((data) => ( 39 | 48 | 57 | 66 | 76 | 83 | 84 | ))} 85 | 86 |
49 | 51 | dispatch(Player.playTrack(data.track)) 52 | } 53 | > 54 | 55 | 56 | 58 | 60 | dispatch(Player.playTrack(data.track)) 61 | } 62 | > 63 | {data.track.name} 64 | 65 | 67 | {data.track.artists.map((artist) => ( 68 | 72 | {artist.name} 73 | 74 | ))} 75 | 77 | 78 | {millisToMinutesAndSeconds( 79 | data.track.duration_ms 80 | )} 81 | 82 |
87 |
88 | ); 89 | } 90 | 91 | export default UserRecentlyPlayed; 92 | -------------------------------------------------------------------------------- /my-app/src/components/UserRecentlyPlayed/styles.css: -------------------------------------------------------------------------------- 1 | #recently-played h2 { 2 | margin-bottom: 20px; 3 | } 4 | 5 | #recently-played table { 6 | border-collapse: collapse; 7 | width: 100%; 8 | } 9 | 10 | #recently-played table tr td { 11 | overflow: hidden; 12 | text-overflow: ellipsis; 13 | white-space: nowrap; 14 | padding: 10px 30px 10px 0; 15 | color: #ccc; 16 | } 17 | 18 | #recently-played table tr td a:hover { 19 | color: #FFF; 20 | } 21 | 22 | #recently-played table tr td span:hover { 23 | color: #FFF; 24 | cursor: pointer; 25 | } 26 | 27 | #recently-played table tr td a + a::before{ 28 | content: ", "; 29 | } 30 | 31 | #recently-played table tr { 32 | border-bottom: 1px solid #282828; 33 | } 34 | 35 | #recently-played table tr:last-child { 36 | border: none; 37 | } 38 | 39 | .track-active span { 40 | color: #1ED760; 41 | } 42 | 43 | #recently-played table .track-active td span:hover { 44 | color: #1ED760; 45 | } 46 | 47 | @media (max-width: 810px) { 48 | #recently-played { 49 | width: 100%; 50 | overflow-x: auto; 51 | } 52 | } 53 | 54 | @media (max-width: 1280px) { 55 | #recently-played { 56 | width: 100%; 57 | overflow-x: auto; 58 | } 59 | } -------------------------------------------------------------------------------- /my-app/src/components/UserTopArtists/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import { Link } from 'react-router-dom'; 4 | 5 | import './styles.css'; 6 | 7 | import api from '../../services/api'; 8 | 9 | function UserTopArtists() { 10 | const [topArtists, setTopArtists] = useState([]); 11 | 12 | async function handleLoad() { 13 | const response = await api.get('/me/top/artists?limit=5'); 14 | 15 | setTopArtists(response.data.items); 16 | } 17 | 18 | useEffect(() => { 19 | handleLoad(); 20 | }, []); 21 | 22 | return ( 23 |
24 |

Seus artistas favoritos

25 |
    26 | {topArtists.map((data) => ( 27 |
  • 28 |
    34 | 35 | {data.name} 36 | 37 |
  • 38 | ))} 39 |
40 |
41 | ); 42 | } 43 | 44 | export default UserTopArtists; 45 | -------------------------------------------------------------------------------- /my-app/src/components/UserTopArtists/styles.css: -------------------------------------------------------------------------------- 1 | #top-artists { 2 | margin-left: 30px; 3 | width: 50%; 4 | } 5 | 6 | #top-artists h2 { 7 | margin-bottom: 20px; 8 | } 9 | 10 | #top-artists ul { 11 | list-style: none; 12 | } 13 | 14 | #top-artists ul li { 15 | display: flex; 16 | align-items: center; 17 | margin-bottom: 15px; 18 | border-bottom: 1px solid #282828; 19 | padding-bottom: 10px; 20 | } 21 | 22 | #top-artists ul li:last-child { 23 | margin-bottom: none; 24 | border: none; 25 | padding: none; 26 | } 27 | 28 | #top-artists ul li .artist-image { 29 | height: 50px; 30 | width: 50px; 31 | background-size: cover; 32 | border-radius: 50px; 33 | margin-right: 20px; 34 | } 35 | 36 | #top-artists ul li span { 37 | font-weight: bold; 38 | font-size: 14px; 39 | color: #ccc; 40 | } 41 | 42 | #top-artists ul li span:hover { 43 | font-weight: bold; 44 | font-size: 14px; 45 | color: #fff; 46 | } 47 | 48 | @media (max-width: 1000px) { 49 | #top-artists { 50 | margin: 20px 0; 51 | width: 100%; 52 | } 53 | } 54 | 55 | @media (max-width: 1280px) { 56 | #top-artists { 57 | width: auto; 58 | } 59 | } 60 | 61 | @media (max-width: 810px) { 62 | #top-artists ul li .artist-image { 63 | height: 60px; 64 | width: 60px; 65 | } 66 | } -------------------------------------------------------------------------------- /my-app/src/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | html, body, input { 8 | font-family: circular-spotify-ui,Helvetica Neue,Helvetica,Arial,sans-serif; 9 | font-size: 16px; 10 | height: 100%; 11 | } 12 | 13 | a { 14 | text-decoration: none; 15 | color: inherit; 16 | } 17 | 18 | button { 19 | border: 0; 20 | font-weight: bold; 21 | padding: 20px; 22 | border-radius: 500px; 23 | text-transform: uppercase; 24 | letter-spacing: 1px; 25 | font-size: 14px; 26 | line-height: 1; 27 | cursor: pointer; 28 | } 29 | 30 | input { 31 | border: 0; 32 | padding: 10px; 33 | font-size: 14px; 34 | } 35 | 36 | #root { 37 | height: 100%; 38 | } 39 | 40 | .container { 41 | padding: 30px; 42 | } 43 | 44 | /* Tracks */ 45 | 46 | .tracks { 47 | width: 100%; 48 | } 49 | 50 | .tracks .track { 51 | display: flex; 52 | flex-direction: row; 53 | align-items: center; 54 | } 55 | 56 | .tracks .track-image { 57 | height: 3rem; 58 | width: 3rem; 59 | background-position: center; 60 | background-size: cover; 61 | margin-right: 15px; 62 | } 63 | 64 | .tracks .track { 65 | width: 100%; 66 | display: flex; 67 | padding: 15px; 68 | transition: .2s; 69 | } 70 | 71 | .track-active { 72 | color: #1ED760; 73 | } 74 | 75 | .track-active:hover { 76 | color: #1ED760; 77 | } 78 | 79 | .tracks .track .note-icon, .play-icon { 80 | margin-right: 15px; 81 | } 82 | 83 | .tracks .track .play-icon { 84 | display: none; 85 | } 86 | 87 | .tracks .track .track-info { 88 | flex: 1; 89 | } 90 | 91 | .tracks .track:hover { 92 | background-color: #282828; 93 | } 94 | 95 | .tracks .track:hover .note-icon { 96 | display: none; 97 | } 98 | 99 | .tracks .track:hover .play-icon { 100 | display: block; 101 | } 102 | 103 | .tracks .track .track-info .track-artists { 104 | color: #888; 105 | font-size: 13px; 106 | margin-top: 5px; 107 | } 108 | 109 | .tracks .track .track-info .track-artists span:hover { 110 | color: #fff; 111 | text-decoration: underline; 112 | text-underline-offset: 5px; 113 | cursor: pointer; 114 | } 115 | 116 | .tracks .track .track-info .track-artists a + a::before { 117 | content: ", "; 118 | } 119 | 120 | .tracks .track .track-duration { 121 | font-weight: bold; 122 | font-size: 15px; 123 | display: flex; 124 | align-items: center; 125 | } 126 | 127 | .copyright { 128 | display: flex; 129 | flex-direction: column; 130 | margin-left: 47px; 131 | margin-top: 20px; 132 | font-size: 12px; 133 | color: #c4c4c4; 134 | } 135 | 136 | /* Cover */ 137 | 138 | .cover { 139 | width: 12rem; 140 | height: 12rem; 141 | background-position: 50%; 142 | background-repeat: no-repeat; 143 | background-size: cover; 144 | } 145 | 146 | /* Grid template */ 147 | 148 | .grid-template { 149 | display: grid; 150 | grid-template-columns: repeat(5, 1fr); 151 | gap: 20px; 152 | } 153 | 154 | /* Table */ 155 | .tracks-table { 156 | width: 100%; 157 | margin-top: 20px; 158 | border-collapse: collapse; 159 | color: #ccc; 160 | } 161 | 162 | .tracks-table thead tr th { 163 | padding: 15px 0; 164 | color: #fff; 165 | } 166 | 167 | .tracks-table tbody tr td { 168 | padding: 20px 0; 169 | } 170 | 171 | .tracks-table tbody tr td:hover { 172 | color: #fff; 173 | } 174 | 175 | .tracks-table tbody .track-active td:hover { 176 | color: inherit; 177 | } 178 | 179 | .tracks-table .track-info { 180 | border-bottom: 1px solid #282828; 181 | } 182 | 183 | .tracks-table .track-info:last-child { 184 | border: 0; 185 | } 186 | 187 | .load-more button { 188 | background-color: rgba(0, 0, 0, 0); 189 | border: 3px solid #FFF; 190 | color: #FFF; 191 | text-transform: capitalize; 192 | } 193 | 194 | table tbody tr td { 195 | padding: 10px 30px 10px 0 !important; 196 | white-space: nowrap; 197 | } 198 | 199 | @media (max-width: 490px) { 200 | .cover { 201 | height: 9rem !important; 202 | width: 9rem !important; 203 | } 204 | } 205 | 206 | @media (max-width: 550px) { 207 | .grid-template { 208 | display: grid; 209 | grid-template-columns: repeat(2, 1fr) !important; 210 | gap: 20px; 211 | } 212 | 213 | .cover { 214 | height: 13rem; 215 | width: 13rem; 216 | } 217 | } 218 | 219 | @media (max-width: 810px) { 220 | .cover { 221 | height: 10rem; 222 | width: 10rem; 223 | } 224 | } 225 | 226 | @media (max-width: 900px) { 227 | .grid-template { 228 | grid-template-columns: repeat(3, 1fr); 229 | } 230 | 231 | .tracks .track-image { 232 | height: 3rem !important; 233 | width: 3rem !important; 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /my-app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /my-app/src/pages/Album/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import { Link, useParams } from 'react-router-dom'; 4 | 5 | import './styles.css'; 6 | 7 | import { FaPlay, FaRegHeart, FaShareAlt, FaHeart } from 'react-icons/fa'; 8 | 9 | import { MdMusicNote } from 'react-icons/md'; 10 | 11 | import { useSelector, useDispatch } from 'react-redux'; 12 | 13 | import api from '../../services/api'; 14 | 15 | import SpotifyButton from '../../components/SpotifyButton'; 16 | 17 | import defaultImage from '../../assets/default-image.jpg'; 18 | 19 | import millisToMinutesAndSeconds from '../../utils/millisToMinutesAndSeconds'; 20 | 21 | import * as Player from '../../store/modules/player/actions'; 22 | 23 | function Album() { 24 | const [album, setAlbum] = useState([]); 25 | 26 | const [save, setSave] = useState(''); 27 | 28 | const [load, setLoad] = useState(true); 29 | 30 | const id = useParams().albumId; 31 | 32 | const dispatch = useDispatch(); 33 | 34 | const trackData = useSelector((state) => state.player.data); 35 | 36 | useEffect(() => { 37 | async function loadAlbum() { 38 | const response = await api.get(`albums/${id}`); 39 | 40 | setAlbum(response.data); 41 | 42 | console.log(response.data); 43 | 44 | setLoad(false); 45 | } 46 | 47 | async function verifySaved() { 48 | const response = await api.get(`/me/albums/contains?ids=${id}`); 49 | 50 | setSave(response.data[0]); 51 | } 52 | 53 | loadAlbum(); 54 | verifySaved(); 55 | }, [id]); 56 | 57 | const date = new Date(album.release_date); 58 | 59 | async function saveAlbum() { 60 | try { 61 | await api.put(`/me/albums?ids=${id}`); 62 | 63 | setSave(true); 64 | } catch (error) { 65 | alert('Ocorreu um erro ao salvar o àlbum.'); 66 | } 67 | } 68 | 69 | async function removeAlbum() { 70 | try { 71 | await api.delete(`/me/albums?ids=${id}`); 72 | 73 | setSave(false); 74 | } catch (error) { 75 | alert('Ocorreu um erro ao remover o àlbum da sua biblioteca'); 76 | } 77 | } 78 | 79 | return ( 80 | <> 81 | {load ? ( 82 |

Carregando...

83 | ) : ( 84 | <> 85 |
86 |
87 |
0 && 92 | album.images[0].url !== '' 93 | ? album.images[0].url 94 | : defaultImage 95 | })`, 96 | }} 97 | /> 98 |

{album.name}

99 |
100 | {album.artists.map((artist) => ( 101 | 105 | {artist.name} 106 | 107 | ))} 108 |
109 | 110 |
111 | {!save && ( 112 | 116 | )} 117 | {save && ( 118 | 123 | )} 124 | 125 |
126 |
127 | {String(date.getFullYear())} 128 | {album.total_tracks === 1 && ( 129 | {album.total_tracks} música 130 | )} 131 | {album.total_tracks > 1 && ( 132 | {album.total_tracks} músicas 133 | )} 134 |
135 |
136 |
137 | {album.tracks.items.map((data) => ( 138 |
147 |
148 | 149 |
150 | 151 |
154 | dispatch( 155 | Player.playTrack({ 156 | id: data.id, 157 | name: data.name, 158 | preview_url: 159 | data.preview_url, 160 | album: { 161 | id: album.id, 162 | images: album.images, 163 | }, 164 | artists: album.artists, 165 | }) 166 | ) 167 | } 168 | > 169 | 170 |
171 |
172 | 173 | {data.name} 174 | 175 |
176 | {data.artists.map((artist) => ( 177 | 181 | {artist.name} 182 | 183 | ))} 184 |
185 |
186 |
187 | {millisToMinutesAndSeconds( 188 | data.duration_ms 189 | )} 190 |
191 |
192 | ))} 193 |
194 | {album.copyrights.map((copyright) => ( 195 | 196 | {copyright.text} 197 | 198 | ))} 199 |
200 |
201 |
202 | 203 | )} 204 | 205 | ); 206 | } 207 | 208 | export default Album; 209 | -------------------------------------------------------------------------------- /my-app/src/pages/Album/styles.css: -------------------------------------------------------------------------------- 1 | #album { 2 | display: flex; 3 | background-repeat: no-repeat; 4 | background-size: cover; 5 | background-position: center; 6 | } 7 | 8 | #album .album-info { 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | width: 23%; 13 | } 14 | 15 | #album .album-info .album-title { 16 | text-align: center; 17 | } 18 | 19 | #album .album-info .album-image { 20 | height: 15rem; 21 | width: 15rem; 22 | background-position: 50%; 23 | margin-bottom: 10px; 24 | } 25 | 26 | #album .album-info span { 27 | color: #c4c4c4; 28 | font-size: 12px; 29 | } 30 | 31 | #album .album-info .album-artists { 32 | margin-top: 5px; 33 | text-align: center; 34 | } 35 | 36 | #album .album-info .album-artists span:hover { 37 | color: #fff; 38 | text-decoration: underline; 39 | text-underline-offset: 5px; 40 | cursor: pointer; 41 | } 42 | 43 | #album .album-info .album-artists a + a::before { 44 | content: ", "; 45 | } 46 | 47 | #album .album-info .album-year { 48 | display: flex; 49 | justify-content: center; 50 | } 51 | 52 | #album .album-info .album-year span + span::before { 53 | content: "-"; 54 | margin: 0 5px; 55 | } 56 | 57 | #album .album-info .album-options { 58 | margin-bottom: 15px; 59 | } 60 | 61 | #album .album-info .album-options svg { 62 | margin: 0 5px; 63 | cursor: pointer; 64 | } 65 | 66 | @media (max-width: 810px) { 67 | #album { 68 | display: flex; 69 | flex-direction: column; 70 | align-items: center; 71 | } 72 | 73 | #album .album-info { 74 | width: 100% !important; 75 | } 76 | 77 | #album .album-tracks { 78 | margin-top: 20px; 79 | margin-left: 0 !important; 80 | } 81 | 82 | .copyright { 83 | margin-left: 0; 84 | margin-top: 10px; 85 | } 86 | 87 | .copyright span + span { 88 | margin-top: 5px; 89 | } 90 | } -------------------------------------------------------------------------------- /my-app/src/pages/Artist/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import { Link, useParams } from 'react-router-dom'; 4 | 5 | import './styles.css'; 6 | 7 | import { FaPlay } from 'react-icons/fa'; 8 | 9 | import { MdMusicNote } from 'react-icons/md'; 10 | 11 | import { useSelector, useDispatch } from 'react-redux'; 12 | 13 | import SpotifyButton from '../../components/SpotifyButton'; 14 | 15 | import api from '../../services/api'; 16 | 17 | import defaultImage from '../../assets/default-image.jpg'; 18 | 19 | import millisToMinutesAndSeconds from '../../utils/millisToMinutesAndSeconds'; 20 | 21 | import * as Player from '../../store/modules/player/actions'; 22 | 23 | function Artist() { 24 | const [artist, setArtist] = useState([]); 25 | const [relatedArtists, setRelatedArtists] = useState([]); 26 | 27 | const [topTracks, setTopTracks] = useState([]); 28 | 29 | const [albums, setAlbums] = useState([]); 30 | 31 | const [following, setFollowing] = useState(''); 32 | 33 | const [load, setLoad] = useState(true); 34 | 35 | const id = useParams().artistId; 36 | 37 | const album = albums.filter((data) => data.album_group === 'album'); 38 | 39 | const single = albums.filter((data) => data.album_group === 'single'); 40 | 41 | const appearsOn = albums.filter( 42 | (data) => data.album_group === 'appears_on' 43 | ); 44 | 45 | const compilation = albums.filter( 46 | (data) => data.album_group === 'compilation' 47 | ); 48 | 49 | const artistsFilter = relatedArtists.slice(0, 10); 50 | 51 | const dispatch = useDispatch(); 52 | 53 | const trackData = useSelector((state) => state.player.data); 54 | 55 | useEffect(() => { 56 | async function loadArtist() { 57 | const response = await api.get(`/artists/${id}`); 58 | 59 | setArtist(response.data); 60 | 61 | setLoad(false); 62 | } 63 | 64 | async function loadTopTracks() { 65 | const response = await api.get( 66 | `/artists/${id}/top-tracks?country=br&limit=50` 67 | ); 68 | 69 | setTopTracks(response.data.tracks); 70 | 71 | setLoad(false); 72 | } 73 | 74 | async function loadAlbums() { 75 | const response = await api.get( 76 | `/artists/${id}/albums?include_groups=album%2Csingle%2Cappears_on%2Ccompilation&market=br&limit=50` 77 | ); 78 | 79 | setAlbums(response.data.items); 80 | 81 | setLoad(false); 82 | } 83 | 84 | async function loadRelatedArtists() { 85 | const response = await api.get(`/artists/${id}/related-artists`); 86 | 87 | setRelatedArtists(response.data.artists); 88 | 89 | setLoad(false); 90 | } 91 | 92 | async function verifyFollowing() { 93 | const response = await api.get( 94 | `/me/following/contains?type=artist&ids=${id}` 95 | ); 96 | 97 | setFollowing(response.data[0]); 98 | 99 | setLoad(false); 100 | } 101 | 102 | loadArtist(); 103 | loadTopTracks(); 104 | loadAlbums(); 105 | loadRelatedArtists(); 106 | 107 | verifyFollowing(); 108 | 109 | document.body.scrollTop = 0; 110 | document.documentElement.scrollTop = 0; 111 | 112 | setLoad(true); 113 | }, [id]); 114 | 115 | async function follow() { 116 | await api.put(`/me/following?type=artist&ids=${id}`); 117 | 118 | setFollowing(true); 119 | } 120 | 121 | async function unfollow() { 122 | await api.delete(`/me/following?type=artist&ids=${id}`); 123 | 124 | setFollowing(false); 125 | } 126 | 127 | return ( 128 |
129 | {load ? ( 130 |

Carregando...

131 | ) : ( 132 | <> 133 |
0 && 137 | artist.images[0].url !== '' 138 | ? artist.images[0].url 139 | : defaultImage 140 | })`, 141 | }} 142 | > 143 |
144 |
0 && 149 | artist.images[0].url !== '' 150 | ? artist.images[0].url 151 | : defaultImage 152 | })`, 153 | }} 154 | /> 155 |
156 |
{artist.type}
157 |
{artist.name}
158 |
159 | {artist.followers.total.toLocaleString( 160 | 'pt-BR' 161 | )}{' '} 162 | seguidores 163 |
164 |
165 |
166 |
167 | 168 | {!following ? ( 169 | 176 | ) : ( 177 | 184 | )} 185 |
186 |
187 |
188 |
189 |
190 |

Populares

191 | {topTracks.map((data) => ( 192 |
201 |
202 | 203 |
204 |
207 | dispatch(Player.playTrack(data)) 208 | } 209 | > 210 | 211 |
212 |
218 |
219 | 220 | {data.name} 221 | 222 |
223 |
224 | {millisToMinutesAndSeconds( 225 | data.duration_ms 226 | )} 227 |
228 |
229 | ))} 230 |
231 |
232 |

Artistas relacionados

233 | {artistsFilter.map((data) => ( 234 |
238 | 239 |
249 | 250 | {data.name} 251 | 252 | 253 |
254 | ))} 255 |
256 |
257 | {album.length > 0 && ( 258 |
259 |

Álbuns

260 |
261 | {album.map((data) => ( 262 | 266 |
272 | {data.name} 273 | 274 | ))} 275 |
276 |
277 | )} 278 | {single.length > 0 && ( 279 |
280 |

Singles e EPs

281 |
282 | {single.map((data) => ( 283 | 287 |
293 | {data.name} 294 | 295 | ))} 296 |
297 |
298 | )} 299 | {compilation.length > 0 && ( 300 |
301 |

Compilações

302 |
303 | {compilation.map((data) => ( 304 | 308 |
314 | {data.name} 315 | 316 | ))} 317 |
318 |
319 | )} 320 | {appearsOn.length > 0 && ( 321 |
322 |

Aparece em

323 |
324 | {appearsOn.map((data) => ( 325 | 329 |
335 | {data.name} 336 | 337 | ))} 338 |
339 |
340 | )} 341 |
342 | 343 | )} 344 |
345 | ); 346 | } 347 | 348 | export default Artist; 349 | -------------------------------------------------------------------------------- /my-app/src/pages/Artist/styles.css: -------------------------------------------------------------------------------- 1 | #artist header { 2 | padding: 0; 3 | background-repeat: no-repeat; 4 | background-size: cover; 5 | background-position: 0 20%; 6 | } 7 | 8 | #artist .artist-info { 9 | padding: 40px 0 0 30px; 10 | display: flex; 11 | flex-direction: row; 12 | } 13 | 14 | #artist .artist-info .artist-bio { 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: center; 18 | font-size: 14px; 19 | } 20 | 21 | #artist .artist-info .artist-bio .type { 22 | text-transform: capitalize; 23 | } 24 | 25 | #artist .artist-info .artist-image { 26 | margin-right: 10px; 27 | } 28 | 29 | #artist .artist-info .artist-bio .artist-name { 30 | font-size: 20px; 31 | font-weight: bold; 32 | margin: 8px 0; 33 | } 34 | 35 | #artist .options { 36 | display: flex; 37 | align-items: center; 38 | margin-left: 30px; 39 | } 40 | 41 | #artist .options button { 42 | margin-left: 10px; 43 | color: #fff; 44 | background-color: rgba(0, 0, 0, 0); 45 | border: 3px solid #fff; 46 | letter-spacing: 1px; 47 | padding-top: 10px; 48 | padding-bottom: 10px; 49 | transition: 0.1s; 50 | } 51 | 52 | #artist .options button:hover { 53 | transform: scale(1.06); 54 | } 55 | 56 | #artist .options .share { 57 | margin-left: 10px; 58 | } 59 | 60 | #artist .tracks-artists .top-tracks h2 { 61 | margin-bottom: 20px; 62 | } 63 | 64 | #artist .tracks-artists .top-tracks .top-tracks-item { 65 | padding: 5px 15px; 66 | } 67 | 68 | #artist .tracks-artists { 69 | display: flex; 70 | } 71 | 72 | #artist .albums { 73 | margin-top: 40px; 74 | } 75 | 76 | #artist .albums h2 { 77 | margin-bottom: 20px; 78 | } 79 | 80 | #artist .albums .album-info { 81 | display: grid; 82 | grid-template-columns: repeat(5, 1fr); 83 | gap: 20px; 84 | } 85 | 86 | #artist .albums .album-info a { 87 | text-align: center; 88 | } 89 | 90 | #artist .albums .album-info .album-cover { 91 | transition: .2s; 92 | margin-bottom: 10px; 93 | } 94 | 95 | #artist .albums .album-info .album-cover:hover { 96 | filter: brightness(.3); 97 | } 98 | 99 | #artist .albums .album-info span { 100 | font-size: 14px; 101 | font-weight: bold; 102 | text-overflow: ellipsis; 103 | overflow: hidden; 104 | -webkit-box-orient: vertical; 105 | display: -webkit-box; 106 | -webkit-line-clamp: 2; 107 | } 108 | 109 | #artist .related-artists { 110 | width: 40%; 111 | margin-left: 50px; 112 | } 113 | 114 | #artist .related-artists h2 { 115 | margin-bottom: 30px; 116 | } 117 | 118 | #artist .related-artists .related-artist a { 119 | display: flex; 120 | align-items: center; 121 | font-size: 14px; 122 | font-weight: bold; 123 | color: #ccc; 124 | margin-bottom: 25px; 125 | } 126 | 127 | #artist .related-artists .related-artist a:hover { 128 | color: #fff; 129 | } 130 | 131 | #artist .related-artists .related-artist .related-artist-cover { 132 | height: 2rem; 133 | width: 2rem; 134 | background-size: cover; 135 | border-radius: 50%; 136 | margin-right: 10px; 137 | } 138 | 139 | @media (max-width: 810px) { 140 | #artist .tracks-artists { 141 | flex-direction: column; 142 | } 143 | 144 | #artist .related-artists { 145 | width: 100%; 146 | margin-left: 0px; 147 | } 148 | 149 | #artist .tracks-artists .top-tracks { 150 | margin-bottom: 30px; 151 | } 152 | 153 | #artist .albums .album-info { 154 | grid-template-columns: repeat(2, 1fr); 155 | } 156 | 157 | #artist .albums .album-info .album-cover { 158 | width: 9rem !important; 159 | height: 9rem !important; 160 | } 161 | 162 | #artist .artist-info .artist-image { 163 | margin-right: 0; 164 | } 165 | 166 | #artist .artist-info { 167 | padding: 0; 168 | padding-top: 15px; 169 | flex-direction: column; 170 | } 171 | 172 | #artist .artist-info .artist-bio { 173 | align-items: center; 174 | } 175 | 176 | #artist .options { 177 | flex-direction: column; 178 | margin-left: 0px; 179 | } 180 | 181 | #artist .options button { 182 | margin-left: 0px; 183 | } 184 | 185 | #artist .related-artists .related-artist .related-artist-cover { 186 | height: 4rem; 187 | width: 4rem; 188 | } 189 | } -------------------------------------------------------------------------------- /my-app/src/pages/Categories/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import { Link, useParams } from 'react-router-dom'; 4 | 5 | import './styles.css'; 6 | 7 | import { IoIosArrowBack, IoIosArrowForward } from 'react-icons/io'; 8 | import api from '../../services/api'; 9 | 10 | import defaultImage from '../../assets/default-image.jpg'; 11 | 12 | function Categories() { 13 | const [category, setCategory] = useState([]); 14 | const [prev, setPrev] = useState(''); 15 | const [next, setNext] = useState(''); 16 | 17 | const [load, setLoad] = useState(true); 18 | 19 | const id = useParams().categoryId; 20 | 21 | useEffect(() => { 22 | async function loadCategory() { 23 | const response = await api.get( 24 | `/browse/categories/${id}/playlists?country=br&limit=50` 25 | ); 26 | 27 | setCategory(response.data.playlists.items); 28 | 29 | setPrev(response.data.playlists.previous); 30 | setNext(response.data.playlists.next); 31 | 32 | setLoad(false); 33 | } 34 | 35 | loadCategory(); 36 | }, [id]); 37 | 38 | async function loadPrevious() { 39 | const replacedEndpointURL = prev.replace( 40 | 'https://api.spotify.com/v1', 41 | '' 42 | ); 43 | 44 | const response = await api.get(replacedEndpointURL); 45 | 46 | setCategory(response.data.playlists.items); 47 | setPrev(response.data.playlists.previous); 48 | setNext(response.data.playlists.next); 49 | 50 | setLoad(false); 51 | } 52 | 53 | async function loadNext() { 54 | const replacedEndpointURL = next.replace( 55 | 'https://api.spotify.com/v1', 56 | '' 57 | ); 58 | 59 | const response = await api.get(replacedEndpointURL); 60 | 61 | setCategory(response.data.playlists.items); 62 | setPrev(response.data.playlists.previous); 63 | setNext(response.data.playlists.next); 64 | 65 | setLoad(false); 66 | } 67 | 68 | return ( 69 |
70 | {load ? ( 71 |

Carregando...

72 | ) : ( 73 | <> 74 |

{id}

75 |
76 |

Playlists populares

77 |
78 | {prev !== null && ( 79 | 82 | )} 83 | {next !== null && ( 84 | 87 | )} 88 |
89 |
90 |
91 | {category.map((data) => ( 92 | 93 | 94 |
95 |
105 |
106 | 107 | {data.name} 108 | 109 | 110 | {data.description} 111 | 112 |
113 |
114 | 115 | 116 | ))} 117 |
118 | 119 | )} 120 |
121 | ); 122 | } 123 | 124 | export default Categories; 125 | -------------------------------------------------------------------------------- /my-app/src/pages/Categories/styles.css: -------------------------------------------------------------------------------- 1 | #categories h1 { 2 | margin-bottom: 30px; 3 | } 4 | 5 | #categories .page-header { 6 | display: flex; 7 | justify-content: space-between; 8 | } 9 | 10 | #categories h1, .page-header h2 { 11 | text-transform: capitalize; 12 | } 13 | 14 | #categories .page-header .pagination button { 15 | background-color: rgba(0, 0, 0, 0.5); 16 | color: #fff; 17 | text-transform: capitalize; 18 | padding: 12px; 19 | margin: 0 2px; 20 | } 21 | 22 | #categories .category{ 23 | display: grid; 24 | grid-template-columns: repeat(5, 195px); 25 | gap: 20px; 26 | margin: 15px 0 30px 0; 27 | } 28 | 29 | #categories .category-item { 30 | display: flex; 31 | flex-direction: column; 32 | } 33 | 34 | #categories .category-item .category-image { 35 | background-position: 50%; 36 | margin-bottom: 10px; 37 | } 38 | 39 | #categories .category-item .category-info { 40 | display: flex; 41 | flex-direction: column; 42 | } 43 | 44 | #categories .category-item .category-info .category-name { 45 | font-weight: bold; 46 | font-size: 14px; 47 | margin: 8px 0; 48 | } 49 | 50 | #categories .category-item .category-info .category-description { 51 | font-size: 13px; 52 | text-overflow: ellipsis; 53 | white-space: nowrap; 54 | color: #ccc; 55 | white-space: normal; 56 | overflow: hidden; 57 | display: -webkit-box; 58 | -webkit-line-clamp: 2; 59 | -webkit-box-orient: vertical; 60 | } 61 | 62 | @media (max-width: 810px) { 63 | #categories .category{ 64 | grid-template-columns: repeat(2, 1fr); 65 | } 66 | 67 | #categories .category-item .category-image { 68 | width: 9rem; 69 | height: 9rem; 70 | } 71 | } -------------------------------------------------------------------------------- /my-app/src/pages/Home/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import './styles.css'; 4 | 5 | import api from '../../services/api'; 6 | 7 | import HomeItems from '../../components/HomeItems'; 8 | 9 | import UserRecentlyPlayed from '../../components/UserRecentlyPlayed'; 10 | 11 | import UserTopArtists from '../../components/UserTopArtists'; 12 | 13 | function Home() { 14 | const [newReleases, setNewReleases] = useState([]); 15 | const [categories, setCategories] = useState([]); 16 | const [playlists, setPlaylists] = useState([]); 17 | 18 | const [load, setLoad] = useState(true); 19 | 20 | async function loadReleases() { 21 | const response = await api.get( 22 | '/browse/new-releases?country=BR&limit=5' 23 | ); 24 | 25 | setNewReleases(response.data.albums.items); 26 | 27 | setLoad(false); 28 | } 29 | 30 | async function loadCategories() { 31 | const response = await api.get('/browse/categories?country=br&limit=5'); 32 | 33 | setCategories(response.data.categories.items); 34 | 35 | setLoad(false); 36 | } 37 | 38 | async function loadPlaylists() { 39 | const response = await api.get( 40 | 'browse/featured-playlists?country=br&limit=5' 41 | ); 42 | 43 | setPlaylists(response.data.playlists.items); 44 | 45 | setLoad(false); 46 | } 47 | 48 | useEffect(() => { 49 | loadReleases(); 50 | loadCategories(); 51 | loadPlaylists(); 52 | }, []); 53 | 54 | return ( 55 |
56 | {load ? ( 57 |

Carregando...

58 | ) : ( 59 | <> 60 | 65 | 70 | 75 |
76 | 77 | 78 |
79 | 80 | )} 81 |
82 | ); 83 | } 84 | 85 | export default Home; 86 | -------------------------------------------------------------------------------- /my-app/src/pages/Home/styles.css: -------------------------------------------------------------------------------- 1 | #home .user-top-lists { 2 | display: flex; 3 | } 4 | 5 | @media (max-width: 1000px) { 6 | #home .user-top-lists { 7 | flex-direction: column; 8 | } 9 | } -------------------------------------------------------------------------------- /my-app/src/pages/Liked/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import { Link } from 'react-router-dom'; 4 | 5 | import { FaPlay } from 'react-icons/fa'; 6 | 7 | import { useSelector, useDispatch } from 'react-redux'; 8 | 9 | import api from '../../services/api'; 10 | 11 | import millisToMinutesAndSeconds from '../../utils/millisToMinutesAndSeconds'; 12 | 13 | import './styles.css'; 14 | 15 | import * as Player from '../../store/modules/player/actions'; 16 | 17 | function Liked() { 18 | const [tracks, setTracks] = useState([]); 19 | 20 | const [next, setNext] = useState(''); 21 | 22 | const [load, setLoad] = useState(true); 23 | 24 | const dispatch = useDispatch(); 25 | 26 | const trackData = useSelector((state) => state.player.data); 27 | 28 | async function loadTracks() { 29 | const response = await api.get('/me/tracks?market=br&limit=50'); 30 | 31 | setTracks(response.data.items); 32 | setNext(response.data.next); 33 | 34 | setLoad(false); 35 | } 36 | 37 | useEffect(() => { 38 | loadTracks(); 39 | }, []); 40 | 41 | async function loadMore() { 42 | const removeEndpointURL = next.replace( 43 | 'https://api.spotify.com/v1', 44 | '' 45 | ); 46 | 47 | const response = await api.get(removeEndpointURL); 48 | 49 | setTracks([...tracks, ...response.data.items]); 50 | } 51 | 52 | return ( 53 | <> 54 | {load ? ( 55 |

Carregando...

56 | ) : ( 57 |
58 |

Músicas que você curtiu

59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | {tracks.map((data) => ( 72 | 81 | 90 | 99 | 106 | 113 | 118 | 119 | ))} 120 | 121 |
#NomeArtistaAlbumDuração
83 | dispatch( 84 | Player.playTrack(data.track) 85 | ) 86 | } 87 | > 88 | 89 | 92 | dispatch( 93 | Player.playTrack(data.track) 94 | ) 95 | } 96 | > 97 | {data.track.name} 98 | 100 | 103 | {data.track.artists[0].name} 104 | 105 | 107 | 110 | {data.track.album.name} 111 | 112 | 114 | {millisToMinutesAndSeconds( 115 | data.track.duration_ms 116 | )} 117 |
122 |
123 | {next != null && ( 124 |
125 | 128 |
129 | )} 130 |
131 | )} 132 | 133 | ); 134 | } 135 | 136 | export default Liked; 137 | -------------------------------------------------------------------------------- /my-app/src/pages/Liked/styles.css: -------------------------------------------------------------------------------- 1 | #liked-tracks .load-more { 2 | margin-top: 50px; 3 | text-align: center; 4 | } 5 | 6 | #liked-tracks .tracks-list .tracks-table .track-info { 7 | cursor: pointer; 8 | } 9 | 10 | @media (max-width: 810px) { 11 | #liked-tracks .tracks-list { 12 | overflow-x: auto; 13 | } 14 | } -------------------------------------------------------------------------------- /my-app/src/pages/Login/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './styles.css'; 4 | 5 | import logo from '../../assets/spotify-clone-app-logo-white.png'; 6 | 7 | function Login() { 8 | return ( 9 |
10 |
11 |
12 | Logo 13 |
14 |
15 |
16 | 23 | 24 | 25 |
26 | Ao clicar no botão acima você será redirecionado para a 27 | página de Login do Spotify 28 |
29 |
30 |
31 | ); 32 | } 33 | 34 | export default Login; 35 | -------------------------------------------------------------------------------- /my-app/src/pages/Login/styles.css: -------------------------------------------------------------------------------- 1 | #login { 2 | height: 100vh; 3 | display: flex; 4 | flex-direction: column; 5 | align-content: center; 6 | justify-content: center; 7 | background-color: #121212; 8 | } 9 | 10 | #login .header { 11 | padding: 25px 0 10px; 12 | margin-bottom: 30px; 13 | text-align: center; 14 | } 15 | 16 | #login .header .spotify-logo img { 17 | width: 200px; 18 | } 19 | 20 | #login .content { 21 | text-align: center; 22 | } 23 | 24 | #login .content button { 25 | background-color: transparent; 26 | color: #FFF; 27 | transition: .1s; 28 | border: 4px solid #FFF; 29 | } 30 | 31 | #login .content button:hover { 32 | background-color: #1ed760; 33 | color: #000; 34 | border-color: #1ed760; 35 | transform: scale(1.06); 36 | } 37 | 38 | #login .content .login-info { 39 | margin: 40px; 40 | font-size: 14px; 41 | color: #c3c3c3; 42 | } -------------------------------------------------------------------------------- /my-app/src/pages/Playlist/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import { Link, useParams } from 'react-router-dom'; 4 | 5 | import './styles.css'; 6 | 7 | import { FaPlay, FaHeart, FaRegHeart, FaShareAlt } from 'react-icons/fa'; 8 | 9 | import { MdMusicNote } from 'react-icons/md'; 10 | 11 | import { useSelector, useDispatch } from 'react-redux'; 12 | 13 | import defaultImage from '../../assets/default-image.jpg'; 14 | 15 | import api from '../../services/api'; 16 | 17 | import SpotifyButton from '../../components/SpotifyButton'; 18 | 19 | import millisToMinutesAndSeconds from '../../utils/millisToMinutesAndSeconds'; 20 | 21 | import * as Player from '../../store/modules/player/actions'; 22 | 23 | function Playlist() { 24 | const [playlist, setPlaylist] = useState([]); 25 | const [save, setSave] = useState(''); 26 | 27 | const [load, setLoad] = useState(true); 28 | 29 | const id = useParams().playlistId; 30 | 31 | const dispatch = useDispatch(); 32 | 33 | const trackData = useSelector((state) => state.player.data); 34 | 35 | useEffect(() => { 36 | async function loadPlaylist() { 37 | const response = await api.get( 38 | `/playlists/${id}?market=br&fields=images%2Chref%2Cname%2Cowner(!href%2Cexternal_urls)%2Ctracks.items(added_by.id%2Ctrack(artists%2Cduration_ms%2Cid%2Cname%2Chref%2Cpreview_url%2Calbum(images%2Cname%2Chref%2Cid)))` 39 | ); 40 | 41 | setPlaylist(response.data); 42 | 43 | setLoad(false); 44 | } 45 | 46 | async function verifySaved() { 47 | const response = await api.get( 48 | `/playlists/${id}/followers/contains?ids=${localStorage.getItem( 49 | 'user' 50 | )}` 51 | ); 52 | 53 | setSave(response.data[0]); 54 | } 55 | 56 | verifySaved(); 57 | loadPlaylist(); 58 | }, [id]); 59 | 60 | async function savePlaylist() { 61 | try { 62 | await api.put(`/playlists/${id}/followers`); 63 | 64 | setSave(true); 65 | } catch (error) { 66 | alert('Ocorreu um erro ao salvar a playlist'); 67 | } 68 | } 69 | 70 | async function removePlaylist() { 71 | try { 72 | await api.delete(`/playlists/${id}/followers`); 73 | 74 | setSave(false); 75 | } catch (error) { 76 | alert('Ocorreu um erro ao remover a playlist da sua biblioteca'); 77 | } 78 | } 79 | 80 | return ( 81 | <> 82 | {load ? ( 83 |

Carregando...

84 | ) : ( 85 |
86 |
87 |
0 92 | ? playlist.images[0].url 93 | : defaultImage 94 | })`, 95 | }} 96 | /> 97 |

{playlist.name}

98 |
99 | 100 | {playlist.owner.display_name} 101 | 102 |
103 | 104 |
105 | {!save && ( 106 | 110 | )} 111 | {save && ( 112 | 117 | )} 118 | 119 |
120 |
121 | {playlist.tracks.length} músicas 122 |
123 |
124 |
125 | {playlist.tracks.items.map((data) => ( 126 |
135 |
136 | 137 |
138 | 139 |
142 | dispatch(Player.playTrack(data.track)) 143 | } 144 | > 145 | 146 |
147 |
148 | 149 | {data.track.name} 150 | 151 |
152 | {data.track.artists.map((artist) => ( 153 | 157 | {artist.name} 158 | 159 | ))} 160 | 163 | 164 | {data.track.album.name} 165 | 166 | 167 |
168 |
169 |
170 | 171 | {millisToMinutesAndSeconds( 172 | data.track.duration_ms 173 | )} 174 | 175 |
176 |
177 | ))} 178 |
179 |
180 | )} 181 | 182 | ); 183 | } 184 | 185 | export default Playlist; 186 | -------------------------------------------------------------------------------- /my-app/src/pages/Playlist/styles.css: -------------------------------------------------------------------------------- 1 | #album { 2 | display: flex; 3 | background-repeat: no-repeat; 4 | background-size: cover; 5 | background-position: center; 6 | } 7 | 8 | #album .album-info { 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | width: 23%; 13 | } 14 | 15 | #album .album-info .album-title { 16 | text-align: center; 17 | } 18 | 19 | #album .album-info .album-image { 20 | background-position: 50%; 21 | margin-bottom: 10px; 22 | } 23 | 24 | #album .album-info span { 25 | color: #c4c4c4; 26 | font-size: 12px; 27 | } 28 | 29 | #album .album-info .album-artists { 30 | margin-top: 5px; 31 | text-align: center; 32 | } 33 | 34 | #album .album-info .album-artists span:hover { 35 | color: #fff; 36 | text-decoration: underline; 37 | text-underline-offset: 5px; 38 | cursor: pointer; 39 | } 40 | 41 | #album .album-info .album-artists a + a::before { 42 | content: ", "; 43 | } 44 | 45 | #album .album-info .album-year { 46 | display: flex; 47 | justify-content: center; 48 | } 49 | 50 | #album .album-info .album-year a + a::before { 51 | content: "-"; 52 | margin: 0 5px; 53 | } 54 | 55 | #album .album-info .album-options { 56 | margin-bottom: 15px; 57 | } 58 | 59 | #album .album-info .album-options svg { 60 | margin: 0 5px; 61 | } 62 | 63 | #album .album-tracks { 64 | margin-left: 30px; 65 | width: 100%; 66 | } -------------------------------------------------------------------------------- /my-app/src/pages/Profile/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import { Link } from 'react-router-dom'; 4 | 5 | import './styles.css'; 6 | 7 | import { FaPlay } from 'react-icons/fa'; 8 | 9 | import { useSelector, useDispatch } from 'react-redux'; 10 | 11 | import api from '../../services/api'; 12 | 13 | import defaultImage from '../../assets/default-image.jpg'; 14 | 15 | import millisToMinutesAndSeconds from '../../utils/millisToMinutesAndSeconds'; 16 | 17 | import * as Player from '../../store/modules/player/actions'; 18 | 19 | function Profile() { 20 | const [user, setUser] = useState([]); 21 | const [artists, setArtists] = useState([]); 22 | const [tracks, setTracks] = useState([]); 23 | 24 | const [recently, setRecently] = useState([]); 25 | 26 | const [tracksTerm, setTracksTerm] = useState('long_term'); 27 | const [artistsTerm, setArtistsTerm] = useState('long_term'); 28 | 29 | const [load, setLoad] = useState(true); 30 | 31 | const dispatch = useDispatch(); 32 | 33 | const trackData = useSelector((state) => state.player.data); 34 | 35 | async function loadUser() { 36 | const response = await api.get(`/me`); 37 | 38 | setUser(response.data); 39 | 40 | setLoad(false); 41 | } 42 | 43 | async function loadRecently() { 44 | const response = await api.get('/me/player/recently-played?limit=10'); 45 | 46 | setRecently(response.data.items); 47 | 48 | setLoad(false); 49 | } 50 | 51 | useEffect(() => { 52 | loadUser(); 53 | loadRecently(); 54 | }, []); 55 | 56 | useEffect(() => { 57 | async function loadTracks() { 58 | const response = await api.get( 59 | `/me/top/tracks?time_range=${tracksTerm}&limit=10` 60 | ); 61 | 62 | setTracks(response.data.items); 63 | 64 | setLoad(false); 65 | } 66 | 67 | loadTracks(); 68 | }, [tracksTerm]); 69 | 70 | useEffect(() => { 71 | async function loadArtists() { 72 | const response = await api.get( 73 | `/me/top/artists?time_range=${artistsTerm}&limit=10` 74 | ); 75 | 76 | setArtists(response.data.items); 77 | 78 | setLoad(false); 79 | } 80 | 81 | loadArtists(); 82 | }, [artistsTerm]); 83 | 84 | return ( 85 | <> 86 | {load ? ( 87 |

Carregando...

88 | ) : ( 89 |
90 |
91 |
101 | {user.display_name} 102 |
103 |
104 |
105 |

Músicas mais escutadas por você

106 |
107 | setTracksTerm('long_term')} 109 | style={{ 110 | color: 111 | tracksTerm === 'long_term' 112 | ? '#FFF' 113 | : '', 114 | }} 115 | > 116 | Todos os tempos 117 | 118 | setTracksTerm('medium_term')} 120 | style={{ 121 | color: 122 | tracksTerm === 'medium_term' 123 | ? '#FFF' 124 | : '', 125 | }} 126 | > 127 | 6 meses 128 | 129 | setTracksTerm('short_term')} 131 | style={{ 132 | color: 133 | tracksTerm === 'short_term' 134 | ? '#FFF' 135 | : '', 136 | }} 137 | > 138 | 4 semanas 139 | 140 |
141 |
142 |
143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | {tracks.map((track) => ( 155 | 164 | 177 | 190 | 197 | 204 | 209 | 210 | ))} 211 | 212 |
#NomeArtistaAlbumDuração
165 | 167 | dispatch( 168 | Player.playTrack( 169 | track 170 | ) 171 | ) 172 | } 173 | > 174 | 175 | 176 | 178 | 180 | dispatch( 181 | Player.playTrack( 182 | track 183 | ) 184 | ) 185 | } 186 | > 187 | {track.name} 188 | 189 | 191 | 194 | {track.artists[0].name} 195 | 196 | 198 | 201 | {track.album.name} 202 | 203 | 205 | {millisToMinutesAndSeconds( 206 | track.duration_ms 207 | )} 208 |
213 |
214 |
215 |
216 |
217 |
218 |

Artistas mais escutados por você

219 |
220 | 222 | setArtistsTerm('long_term') 223 | } 224 | style={{ 225 | color: 226 | artistsTerm === 'long_term' 227 | ? '#FFF' 228 | : '', 229 | }} 230 | > 231 | Todos os tempos 232 | 233 | 235 | setArtistsTerm('medium_term') 236 | } 237 | style={{ 238 | color: 239 | artistsTerm === 'medium_term' 240 | ? '#FFF' 241 | : '', 242 | }} 243 | > 244 | 6 meses 245 | 246 | 248 | setArtistsTerm('short_term') 249 | } 250 | style={{ 251 | color: 252 | artistsTerm === 'short_term' 253 | ? '#FFF' 254 | : '', 255 | }} 256 | > 257 | 4 semanas 258 | 259 |
260 |
261 |
262 | {artists.map((artist) => ( 263 |
264 |
270 | 271 | 274 | {artist.name} 275 | 276 | 277 |
278 | ))} 279 |
280 |
281 |
282 |
283 |

Tocadas recentemente

284 |
285 |
286 | {recently.map((data) => ( 287 |
288 |
294 | {data.track.name} 295 |
296 | ))} 297 |
298 |
299 |
300 |
301 | )} 302 | 303 | ); 304 | } 305 | 306 | export default Profile; 307 | -------------------------------------------------------------------------------- /my-app/src/pages/Profile/styles.css: -------------------------------------------------------------------------------- 1 | #profile .header { 2 | display: flex; 3 | align-items: center; 4 | flex-direction: column; 5 | margin-bottom: 50px; 6 | } 7 | 8 | #profile h2 { 9 | margin-bottom: 0 !important; 10 | } 11 | 12 | #profile .header .user-picture { 13 | border-radius: 50%; 14 | margin-bottom: 10px; 15 | } 16 | 17 | #profile .header .username { 18 | font-weight: bold; 19 | text-transform: capitalize; 20 | font-size: 30px; 21 | } 22 | 23 | #profile .options-header { 24 | display: flex; 25 | flex-direction: row; 26 | justify-content: space-between; 27 | align-items: center; 28 | margin-bottom: 20px; 29 | } 30 | 31 | #profile .tracks .tracks-list .tracks-table span { 32 | cursor: pointer; 33 | } 34 | 35 | #profile .options-header .options span { 36 | margin-left: 10px; 37 | color: #ccc; 38 | font-weight: bold; 39 | } 40 | 41 | #profile .options-header .options span:hover { 42 | color: #fff; 43 | cursor: pointer; 44 | } 45 | 46 | #profile .tracks { 47 | margin-bottom: 30px; 48 | } 49 | 50 | #profile .artists-and-recently { 51 | display: flex; 52 | } 53 | 54 | #profile .artists-and-recently .artists { 55 | display: flex; 56 | flex-direction: column; 57 | } 58 | 59 | #profile .artists-and-recently .artists .artists-list .artist { 60 | display: flex; 61 | align-items: center; 62 | padding: 15px 0; 63 | border-bottom: 1px solid #282828; 64 | } 65 | 66 | #profile .artists-and-recently .artists .artists-list .artist:last-child { 67 | border: 0; 68 | } 69 | 70 | #profile .artists-and-recently .artists .artists-list .artist .artist-cover { 71 | height: 3rem; 72 | width: 3em; 73 | border-radius: 50%; 74 | margin-right: 10px; 75 | } 76 | 77 | #profile .artists-and-recently .artists .artists-list .artist span { 78 | font-weight: bold; 79 | color: #ccc; 80 | } 81 | 82 | #profile .artists-and-recently .artists .artists-list .artist span:hover { 83 | color: #fff; 84 | } 85 | 86 | #profile .artists-and-recently .recently-played { 87 | margin-left: 30px; 88 | width: 30%; 89 | } 90 | 91 | #profile .artists-and-recently .recently-played .recently-list .track { 92 | display: flex; 93 | align-items: center; 94 | margin-bottom: 15px; 95 | } 96 | 97 | #profile .artists-and-recently .recently-played .recently-list .track span { 98 | text-overflow: ellipsis; 99 | white-space: nowrap; 100 | overflow: hidden; 101 | max-width: 50%; 102 | } 103 | 104 | #profile .artists-and-recently .recently-played .recently-list .track .track-cover { 105 | width: 4rem; 106 | height: 4rem; 107 | margin-right: 10px; 108 | } 109 | 110 | @media (max-width: 810px) { 111 | #profile .tracks .tracks-list { 112 | overflow-x: auto; 113 | } 114 | 115 | #profile .options-header { 116 | flex-direction: column !important; 117 | align-items: flex-start !important; 118 | } 119 | 120 | #profile .options-header .options { 121 | margin-top: 20px; 122 | } 123 | 124 | #profile .options-header .options span { 125 | margin-left: 0 !important; 126 | } 127 | 128 | #profile .options-header .options span + span { 129 | margin-left: 10px !important; 130 | } 131 | 132 | #profile .artists-and-recently { 133 | flex-direction: column; 134 | } 135 | 136 | #profile .artists-and-recently .artists { 137 | margin-bottom: 30px; 138 | } 139 | 140 | #profile .artists-and-recently .recently-played { 141 | margin-left: 0px; 142 | width: 100%; 143 | } 144 | 145 | #profile .artists-and-recently .artists .artists-list .artist .artist-cover { 146 | height: 4rem !important; 147 | width: 4rem !important; 148 | } 149 | 150 | #profile .artists-and-recently .recently-played .recently-list .track .track-cover { 151 | width: 4rem !important; 152 | height: 4rem !important; 153 | } 154 | } -------------------------------------------------------------------------------- /my-app/src/pages/Recently/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import { Link } from 'react-router-dom'; 4 | 5 | import './styles.css'; 6 | 7 | import { useDispatch } from 'react-redux'; 8 | 9 | import api from '../../services/api'; 10 | 11 | import * as Player from '../../store/modules/player/actions'; 12 | 13 | export default function Recently() { 14 | const dispatch = useDispatch(); 15 | const [recently, setRecently] = useState([]); 16 | 17 | const [load, setLoad] = useState(true); 18 | 19 | async function loadRecently() { 20 | const response = await api.get('/me/player/recently-played?limit=50'); 21 | 22 | setRecently(response.data.items); 23 | 24 | setLoad(false); 25 | } 26 | 27 | useEffect(() => { 28 | loadRecently(); 29 | }, []); 30 | 31 | return ( 32 | <> 33 | {load ? ( 34 |

Carregando...

35 | ) : ( 36 |
37 |

Tocadas recentemente

38 |
39 | {recently.map((data) => ( 40 |
41 |
47 | dispatch(Player.playTrack(data.track)) 48 | } 49 | /> 50 | 51 | {data.track.name} 52 | 53 |
54 | 55 | 58 | {data.track.artists[0].name} 59 | 60 | 61 | 62 | 65 | {data.track.album.name} 66 | 67 | 68 |
69 |
70 | ))} 71 |
72 |
73 | )} 74 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /my-app/src/pages/Recently/styles.css: -------------------------------------------------------------------------------- 1 | #recently { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | #recently h2 { 7 | margin-bottom: 20px; 8 | } 9 | 10 | #recently .track { 11 | text-align: center; 12 | } 13 | 14 | #recently .track .cover { 15 | margin-bottom: 10px; 16 | transition: .2s; 17 | cursor: pointer; 18 | } 19 | 20 | #recently .track .cover:hover { 21 | filter: brightness(.3); 22 | } 23 | 24 | #recently .track .track-name { 25 | font-weight: bold; 26 | font-size: 14px; 27 | text-overflow: ellipsis; 28 | overflow: hidden; 29 | -webkit-box-orient: vertical; 30 | display: -webkit-box; 31 | -webkit-line-clamp: 2; 32 | } 33 | 34 | 35 | #recently .track .artist-and-album { 36 | margin-top: 5px; 37 | display: flex; 38 | flex-direction: column; 39 | } 40 | 41 | #recently .track .artist-and-album span { 42 | font-size: 13px; 43 | margin-bottom: 5px; 44 | color: #ccc; 45 | text-overflow: ellipsis; 46 | overflow: hidden; 47 | -webkit-box-orient: vertical; 48 | display: -webkit-box; 49 | -webkit-line-clamp: 2; 50 | } 51 | 52 | #recently .track .artist-and-album span:hover { 53 | text-decoration: underline; 54 | text-underline-offset: 2px; 55 | } 56 | 57 | @media (max-width: 810px) { 58 | #recently .track .cover { 59 | width: 9rem; 60 | height: 9rem; 61 | } 62 | } 63 | 64 | @media (max-height: 450px) { 65 | #recently .track .cover { 66 | width: 100%; 67 | height: 13rem; 68 | } 69 | } -------------------------------------------------------------------------------- /my-app/src/pages/Search/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { debounce } from 'lodash'; 4 | 5 | import { Link } from 'react-router-dom'; 6 | 7 | import { FiSearch } from 'react-icons/fi'; 8 | 9 | import './styles.css'; 10 | 11 | import api from '../../services/api'; 12 | 13 | import defaultImage from '../../assets/default-image.jpg'; 14 | 15 | export default function Search() { 16 | const [artists, setArtists] = useState([]); 17 | const [albums, setAlbums] = useState([]); 18 | const [playlists, setPlaylists] = useState([]); 19 | 20 | async function load(value) { 21 | const response = await api.get( 22 | `/search?q=${value}&type=album%2Cartist%2Cplaylist%2Ctrack&market=br` 23 | ); 24 | 25 | setArtists(response.data.artists.items); 26 | setAlbums(response.data.albums.items); 27 | setPlaylists(response.data.playlists.items); 28 | } 29 | const delayedSearch = debounce((value) => { 30 | load(value); 31 | }, 1000); 32 | 33 | return ( 34 |