├── .babelrc ├── tests ├── main.spec.js ├── Spotify.spec.js ├── ConvertToHumanTime.spec.js ├── AlbumInfo.spec.js ├── AlbumTracks.spec.js └── AlbumList.spec.js ├── example ├── screenshot.png ├── index.html └── main.css ├── .editorconfig ├── src ├── main.js ├── ConvertToHumanTime.js ├── Spotify.js ├── AlbumInfo.js ├── AlbumTracks.js ├── SearchTrigger.js ├── AlbumList.js ├── SelectAlbumTrigger.js └── PlaylistTrigger.js ├── .eslintrc.js ├── CONTRIBUTING.md ├── webpack.config.js ├── .gitignore ├── LICENSE.md ├── package.json └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: ['env'] 3 | } 4 | -------------------------------------------------------------------------------- /tests/main.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | describe('Main', () => { 4 | 5 | }); 6 | -------------------------------------------------------------------------------- /example/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/spotify-wrapper-player/HEAD/example/screenshot.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import searchEnterTrigger from './SearchTrigger'; 2 | import selectAlbumTrigger from './SelectAlbumTrigger'; 3 | import playlistTrigger from './PlaylistTrigger'; 4 | 5 | searchEnterTrigger(); 6 | selectAlbumTrigger(); 7 | playlistTrigger(); 8 | -------------------------------------------------------------------------------- /src/ConvertToHumanTime.js: -------------------------------------------------------------------------------- 1 | export default function convertToHumanTime(duration) { 2 | let s = parseInt((duration / 1000) % 60, 10); 3 | const m = parseInt((duration / (1000 * 60)) % 60, 10); 4 | 5 | s = (s < 10) ? `0${s}` : s; 6 | 7 | return `${m}:${s}`; 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb", 3 | "plugins": [ 4 | "react", 5 | "jsx-a11y", 6 | "import" 7 | ], 8 | "env": { 9 | "browser": true 10 | }, 11 | "rules": { 12 | "no-param-reassign": 0 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/Spotify.js: -------------------------------------------------------------------------------- 1 | import SpotifyWrapper from 'spotify-wrapper'; 2 | 3 | const spotify = new SpotifyWrapper({ 4 | token: 'BQALqTsjYqIXtLNUom9ETZc1E9iph30igV8GySCIGqLkrtQR-unagwWiqryhnLY-cnjInpTj2hMhefM4A_S0t9pfFf2qsdjVKXvgucykeeXH-mxIwH19gnbIxLk41kwGBKOr5nfEFeyPoQZW72U', 5 | }); 6 | 7 | export default spotify; 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. Fork it! 4 | 2. Create your feature branch: `git checkout -b my-new-feature` 5 | 3. Commit your changes: `git commit -m 'Add some feature'` 6 | 4. Push to the branch: `git push origin my-new-feature` 7 | 8 | *Remember that we have a pre-push hook with steps that analyzes and prevents mistakes.* 9 | 10 | **After your pull request is merged**, you can safely delete your branch. 11 | -------------------------------------------------------------------------------- /tests/Spotify.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import spotify from '../src/Spotify'; 3 | 4 | describe('Spotify', () => { 5 | it('should be an object', () => { 6 | expect(spotify).to.be.an.object; 7 | }); 8 | 9 | it('should have search methods', () => { 10 | expect(spotify.search).to.exist; 11 | }); 12 | 13 | it('should have album methods', () => { 14 | expect(spotify.album).to.exist; 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/AlbumInfo.js: -------------------------------------------------------------------------------- 1 | function createMarkup(data) { 2 | return (` 3 | ${data.name} 4 |

${data.name}

5 |

${data.artists[0].name}

6 |

${data.tracks.total} Músicas

7 | `); 8 | } 9 | 10 | export default function renderAlbumInfo(data, element) { 11 | const markup = createMarkup(data); 12 | element.innerHTML = markup; 13 | 14 | return data; 15 | } 16 | -------------------------------------------------------------------------------- /src/AlbumTracks.js: -------------------------------------------------------------------------------- 1 | import convertToHumanTime from './ConvertToHumanTime'; 2 | 3 | function createMarkup(tracks) { 4 | return tracks.map(track => ` 5 |
6 |

${track.track_number}

7 |

${track.name}

8 |

${convertToHumanTime(track.duration_ms)}

9 |
`).join(''); 10 | } 11 | 12 | export default function renderAlbumTracks(data, element) { 13 | const markup = createMarkup(data); 14 | element.innerHTML = markup; 15 | } 16 | -------------------------------------------------------------------------------- /src/SearchTrigger.js: -------------------------------------------------------------------------------- 1 | import spotify from './Spotify'; 2 | import renderAlbums from './AlbumList'; 3 | 4 | const albumList = document.getElementById('album-list'); 5 | const searchInput = document.getElementById('search-input'); 6 | const searchForm = document.getElementById('search-form'); 7 | 8 | function makeRequest() { 9 | spotify.search.albums(searchInput.value) 10 | .then(data => renderAlbums(data.albums.items, albumList)); 11 | } 12 | 13 | export default function searchEnterTrigger() { 14 | searchForm.addEventListener('submit', (e) => { 15 | e.preventDefault(); 16 | makeRequest(); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | context: path.resolve(__dirname, './src'), 6 | entry: { 7 | app: './main.js', 8 | }, 9 | output: { 10 | filename: 'bundle.js', 11 | path: path.resolve(__dirname, './example'), 12 | }, 13 | devServer: { 14 | contentBase: path.resolve(__dirname, './example'), 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js$/, 20 | exclude: [/node_modules/], 21 | use: [{ 22 | loader: 'babel-loader', 23 | }] 24 | } 25 | ] 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/AlbumList.js: -------------------------------------------------------------------------------- 1 | function createMarkup(data) { 2 | return data.map(album => ` 3 |
4 | ${album.name} 5 |
6 |

${album.name}

7 |

${album.artists[0].name}

8 |
9 |
`).join(''); 10 | } 11 | 12 | export default function renderAlbums(data, element) { 13 | const markup = createMarkup(data); 14 | 15 | element.innerHTML = markup; 16 | } 17 | -------------------------------------------------------------------------------- /src/SelectAlbumTrigger.js: -------------------------------------------------------------------------------- 1 | import spotify from './Spotify'; 2 | import renderAlbumInfo from './AlbumInfo'; 3 | import renderAlbumTracks from './AlbumTracks'; 4 | 5 | const listAlbums = document.getElementById('album-list'); 6 | const albumInfo = document.getElementById('album-info'); 7 | const albumTracks = document.getElementById('album-tracks'); 8 | 9 | function makeRequest(albumId) { 10 | spotify.album.getAlbum(albumId) 11 | .then(data => renderAlbumInfo(data, albumInfo)) 12 | .then(data => renderAlbumTracks(data.tracks.items, albumTracks)); 13 | } 14 | 15 | export default function selectAlbumTrigger() { 16 | listAlbums.addEventListener('click', (e) => { 17 | const target = e.target; 18 | makeRequest(target.getAttribute('data-album-id')); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /tests/ConvertToHumanTime.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import convertToHumanTime from '../src/ConvertToHumanTime'; 3 | 4 | describe('ConvertToHumanTime', () => { 5 | // 0ms === 0:00 6 | it('should receive 0ms and convert to 0:00', () => { 7 | expect(convertToHumanTime(0)).to.be.equal('0:00'); 8 | }); 9 | 10 | // 1000ms === 0:01 11 | it('should receive 1000ms and convert to 0:01', () => { 12 | expect(convertToHumanTime(1000)).to.be.equal('0:01'); 13 | }); 14 | 15 | // 11000ms === 0:11 16 | it('should receice 11000ms and convert to 0:11', () => { 17 | expect(convertToHumanTime(11000)).to.be.equal('0:11'); 18 | }); 19 | 20 | // 60000ms === 1:00 21 | it('should receive 60000ms and convert to 1:00', () => { 22 | expect(convertToHumanTime(60000)).to.be.equal('1:00'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional eslint cache 38 | .eslintcache 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | 43 | # Output of 'npm pack' 44 | *.tgz 45 | -------------------------------------------------------------------------------- /src/PlaylistTrigger.js: -------------------------------------------------------------------------------- 1 | const albumTracks = document.getElementById('album-tracks'); 2 | let audioObject = null; 3 | 4 | export default function playlistTrigger() { 5 | albumTracks.addEventListener('click', (e) => { 6 | const target = e.target.parentNode; 7 | 8 | if (target.classList.contains('active')) { 9 | audioObject.pause(); 10 | } else { 11 | if (audioObject) { 12 | audioObject.pause(); 13 | } 14 | audioObject = new Audio(target.getAttribute('data-track-preview')); 15 | audioObject.play(); 16 | target.classList.add('active'); 17 | 18 | audioObject.addEventListener('ended', () => { 19 | target.classList.remove('active'); 20 | }); 21 | 22 | audioObject.addEventListener('pause', () => { 23 | target.classList.remove('active'); 24 | }); 25 | } 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 - Willian Justen 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /tests/AlbumInfo.spec.js: -------------------------------------------------------------------------------- 1 | import 'jsdom-global/register'; 2 | import { expect } from 'chai'; 3 | 4 | import renderAlbumInfo from '../src/AlbumInfo'; 5 | 6 | describe('AlbumInfo', () => { 7 | 8 | const data = { 9 | "album_type" : "album", 10 | "artists" : [ { 11 | "name" : "Incubus", 12 | } ], 13 | "id" : "6peEdPVO73WtgGah5sEhX4", 14 | "images" : [ { 15 | "height" : 640, 16 | "url" : "https://i.scdn.co/image/59a536f0bf0ddaa427e4c732a061c33fe7578757", 17 | "width" : 640 18 | }, { 19 | "height" : 300, 20 | "url" : "https://i.scdn.co/image/9d6866c93e476bd8e7aa7771f9b68db119e076c6", 21 | "width" : 300 22 | }, { 23 | "height" : 64, 24 | "url" : "https://i.scdn.co/image/3ad2701e3f6fe51404f3a4de7a5b2c7b745bad16", 25 | "width" : 64 26 | } ], 27 | "name" : "The Essential Incubus", 28 | "type" : "album", 29 | "tracks": { 30 | "total": 18 31 | } 32 | }; 33 | 34 | const markup = ` 35 | The Essential Incubus 36 |

The Essential Incubus

37 |

Incubus

38 |

18 Músicas

39 | `; 40 | 41 | it('should create and append the markup given a correct data', () => { 42 | const element = document.createElement('div'); 43 | renderAlbumInfo(data, element); 44 | 45 | expect(element.innerHTML).to.be.eql(markup); 46 | }); 47 | 48 | it('should return the data', () => { 49 | const element2 = document.createElement('div'); 50 | expect(renderAlbumInfo(data, element2)).to.be.eql(data); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spotify-wrapper-player", 3 | "version": "1.0.0", 4 | "description": "A player using the spotify-wrapper lib.", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "./node_modules/.bin/eslint src/*.js", 8 | "prepush": "npm run lint", 9 | "test": "./node_modules/.bin/mocha tests/**/*.spec.js --require babel-register", 10 | "test:tdd": "./node_modules/.bin/mocha tests/**/*.spec.js --require babel-register --watch", 11 | "test:coverage": "nyc npm test", 12 | "start": "./node_modules/.bin/webpack-dev-server --open" 13 | }, 14 | "nyc": { 15 | "reporter": [ 16 | "text", 17 | "html" 18 | ], 19 | "exclude": [ 20 | "tests/**" 21 | ] 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/willianjusten/spotify-wrapper-player.git" 26 | }, 27 | "keywords": [ 28 | "js", 29 | "tdd", 30 | "library" 31 | ], 32 | "author": "Willian Justen (https://willianjusten.com.br/)", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/willianjusten/spotify-wrapper-player/issues" 36 | }, 37 | "homepage": "https://github.com/willianjusten/spotify-wrapper-player#readme", 38 | "devDependencies": { 39 | "babel-loader": "^7.0.0", 40 | "babel-preset-env": "^1.3.2", 41 | "babel-register": "^6.24.0", 42 | "chai": "^3.5.0", 43 | "eslint": "^3.7.1", 44 | "eslint-config-airbnb": "^12.0.0", 45 | "eslint-plugin-import": "^2.0.1", 46 | "eslint-plugin-jsx-a11y": "^2.2.3", 47 | "eslint-plugin-react": "^6.4.1", 48 | "husky": "^0.11.9", 49 | "jsdom": "^11.0.0", 50 | "jsdom-global": "^3.0.2", 51 | "mocha": "^3.2.0", 52 | "nyc": "^10.2.0", 53 | "webpack": "^2.6.1", 54 | "webpack-dev-server": "^2.4.5" 55 | }, 56 | "dependencies": { 57 | "spotify-wrapper": "^2.0.2" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spotify Wrapper Player 6 | 7 | 8 | 9 | 10 | 30 | 31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spotify Wrapper Player 2 | 3 | ![Spotify Wrapper Player Screenshot](example/screenshot.png) 4 | 5 | > This application uses [spotify-wrapper](https://github.com/willianjusten/spotify-wrapper) library to get informations and songs from Spotify. It was created on my [JS TDD course](https://willianjusten.com.br/cursos/) just for study purposes. 6 | 7 | ## Browser Support 8 | 9 | This Application relies on [Fetch API](https://fetch.spec.whatwg.org/). And this API is supported in the following browsers. 10 | 11 | ![Chrome](https://cloud.githubusercontent.com/assets/398893/3528328/23bc7bc4-078e-11e4-8752-ba2809bf5cce.png) | ![Firefox](https://cloud.githubusercontent.com/assets/398893/3528329/26283ab0-078e-11e4-84d4-db2cf1009953.png) | ![Opera](https://cloud.githubusercontent.com/assets/398893/3528330/27ec9fa8-078e-11e4-95cb-709fd11dac16.png) | ![Safari](https://cloud.githubusercontent.com/assets/398893/3528331/29df8618-078e-11e4-8e3e-ed8ac738693f.png) | ![IE](https://cloud.githubusercontent.com/assets/398893/3528325/20373e76-078e-11e4-8e3a-1cb86cf506f0.png) | 12 | --- | --- | --- | --- | --- | 13 | 39+ ✔ | 42+ ✔ | 29+ ✔ | 10.1+ ✔ | Nope ✘ | 14 | 15 | ## How to Run 16 | 17 | 1. First go to [Spotify Developers API](https://developer.spotify.com/web-api/) and create your Token. Then add your token on [src/Spotify.js](src/Spotify.js). **Remember that the token will expire in 60min** 18 | 2. Install the dependencies with `npm i`. 19 | 3. Run your application with `npm start`. 20 | 21 | ## Something in the future? 22 | 23 | - [ ] Create authentication method to get Token 24 | - [ ] Show similar artists 25 | - [ ] Create player buttons (prev, pause/play, pause) 26 | - [ ] Show music timeline 27 | - [ ] Be creative =D 28 | 29 | ## Contributing 30 | 31 | Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. 32 | 33 | ## Versioning 34 | 35 | We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/willianjusten/spotify-wrapper-player/tags). 36 | 37 | ## Authors 38 | 39 | | ![Willian Justen](https://avatars2.githubusercontent.com/u/3991845?v=3&s=150)| 40 | |:---------------------:| 41 | | [Willian Justen](https://github.com/willianjusten/) | 42 | 43 | See also the list of [contributors](https://github.com/willianjusten/spotify-wrapper-player/contributors) who participated in this project. 44 | 45 | ## License 46 | 47 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 48 | 49 | -------------------------------------------------------------------------------- /tests/AlbumTracks.spec.js: -------------------------------------------------------------------------------- 1 | import 'jsdom-global/register'; 2 | import { expect } from 'chai'; 3 | 4 | import renderAlbumTracks from '../src/AlbumTracks'; 5 | import convertToHumanTime from '../src/ConvertToHumanTime'; 6 | 7 | describe('AlbumTracks', () => { 8 | const data = [ 9 | { 10 | "preview_url": "https://p.scdn.co/mp3-preview/ab3d501c5ffbf560e94094f76cd36d874a26e941?cid=8897482848704f2a8f8d7c79726a70d4", 11 | "track_number": 1, 12 | "name": "Around The World", 13 | "duration_ms": 238733 14 | } 15 | ]; 16 | 17 | const data2 = [ 18 | { 19 | "preview_url": "https://p.scdn.co/mp3-preview/ab3d501c5ffbf560e94094f76cd36d874a26e941?cid=8897482848704f2a8f8d7c79726a70d4", 20 | "track_number": 1, 21 | "name": "Around The World", 22 | "duration_ms": 238733 23 | }, 24 | { 25 | "preview_url": "https://p.scdn.co/mp3-preview/ab3d501c5ffbf560e94094f76cd36d874a26e941?cid=8897482848704f2a8f8d7c79726a70d4", 26 | "track_number": 1, 27 | "name": "Around The World", 28 | "duration_ms": 238733 29 | } 30 | ]; 31 | 32 | const markup = ` 33 |
34 |

1

35 |

Around The World

36 |

${convertToHumanTime(238733)}

37 |
`; 38 | 39 | const markup2 = ` 40 |
41 |

1

42 |

Around The World

43 |

${convertToHumanTime(238733)}

44 |
45 |
46 |

1

47 |

Around The World

48 |

${convertToHumanTime(238733)}

49 |
`; 50 | 51 | it('should create and append the markup given a correct data', () => { 52 | const element = document.createElement('div'); 53 | renderAlbumTracks(data, element); 54 | 55 | expect(element.innerHTML).to.be.eql(markup); 56 | }); 57 | 58 | it('should create and append the markup when more than 1 item', () => { 59 | const element2 = document.createElement('div'); 60 | renderAlbumTracks(data2, element2); 61 | 62 | expect(element2.innerHTML).to.be.eql(markup2); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /example/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | background: #121212; 8 | font-family: 'Lato', sans-serif; 9 | color: #a0a0a0; 10 | display: flex; 11 | overflow: hidden; 12 | } 13 | 14 | .aside { 15 | background: #0f1015; 16 | box-shadow: 0px 5px 8px #0f1015; 17 | height: 100vh; 18 | width: 350px; 19 | overflow: scroll; 20 | } 21 | 22 | .aside-container { 23 | padding: 20px; 24 | } 25 | 26 | .aside-header { 27 | margin-bottom: 20px; 28 | } 29 | 30 | .aside-link { 31 | align-items: center; 32 | color: #fff; 33 | display: flex; 34 | height: 32px; 35 | text-decoration: none; 36 | transition: color .4s; 37 | } 38 | 39 | .aside-link:hover { 40 | color: #1ed760; 41 | } 42 | 43 | .aside-icon { 44 | margin-right: 10px; 45 | width: 32px; 46 | } 47 | 48 | .aside-title { 49 | font-size: 16px; 50 | } 51 | 52 | .form-wrapper { 53 | align-items: center; 54 | border-top: 1px solid rgba(255, 255, 255, .25); 55 | border-bottom: 1px solid rgba(255, 255, 255, .25); 56 | display: flex; 57 | padding: 10px 0; 58 | } 59 | 60 | .form-input { 61 | background: none; 62 | border: none; 63 | border-radius: 30px; 64 | color: white; 65 | font-size: 14px; 66 | font-weight: bold; 67 | margin-right: 10px; 68 | outline: none; 69 | padding: 12px 20px; 70 | } 71 | 72 | .form-input:focus { 73 | color: #1ed760; 74 | } 75 | 76 | .form-input:focus + .form-icon { 77 | color: #1ed760; 78 | } 79 | 80 | .form-icon { 81 | cursor: pointer; 82 | width: 22px; 83 | } 84 | 85 | .list-item { 86 | align-items: center; 87 | cursor: pointer; 88 | display: flex; 89 | min-height: 70px; 90 | opacity: .7; 91 | padding: 10px 20px; 92 | transition: opacity .4s; 93 | } 94 | 95 | .list-item:nth-of-type(even) { 96 | background: rgba(29, 32, 40, 0.61); 97 | } 98 | 99 | .list-item:hover { 100 | opacity: 1; 101 | } 102 | 103 | .list-image { 104 | height: 50px; 105 | width: 50px; 106 | margin-right: 10px; 107 | } 108 | 109 | .list-title { 110 | color: #fff; 111 | } 112 | 113 | .container { 114 | background-image: linear-gradient(rgb(70, 76, 82), rgb(7, 7, 8) 85%); 115 | height: 100vh; 116 | overflow: scroll; 117 | width: 100%; 118 | } 119 | 120 | .container-inner { 121 | display: flex; 122 | padding: 50px; 123 | } 124 | 125 | .album-info { 126 | margin-right: 50px; 127 | text-align: center; 128 | } 129 | 130 | .album-image { 131 | width: 250px; 132 | height: 250px; 133 | } 134 | 135 | .album-title { 136 | color: #fff; 137 | font-size: 24px; 138 | margin: 20px 0 0; 139 | } 140 | 141 | .album-artist { 142 | font-size: 18px; 143 | } 144 | 145 | .album-counter { 146 | font-size: 14px; 147 | } 148 | 149 | .album-artist, 150 | .album-counter { 151 | font-weight: 300; 152 | margin-bottom: 25px; 153 | } 154 | 155 | .play-btn { 156 | background: #1ed760; 157 | border-radius: 30px; 158 | color: #fff; 159 | font-size: 14px; 160 | font-weight: 300; 161 | padding: 13px 44px; 162 | text-decoration: none; 163 | text-transform: uppercase; 164 | transition: background .3s; 165 | } 166 | 167 | .play-btn:hover { 168 | background: #16a248; 169 | } 170 | 171 | .album-musics { 172 | width: 100%; 173 | } 174 | 175 | .music { 176 | cursor: pointer; 177 | display: flex; 178 | font-size: 18px; 179 | font-weight: 300; 180 | transition: background .3s; 181 | width: 100%; 182 | } 183 | 184 | .music.active { 185 | color: #16a248; 186 | } 187 | 188 | .music.active .music-title { 189 | color: #16a248; 190 | } 191 | 192 | .music:hover { 193 | background: #000; 194 | } 195 | 196 | .music-number, 197 | .music-title, 198 | .music-duration { 199 | padding: 10px; 200 | } 201 | 202 | .music-number { 203 | margin-right: 20px; 204 | min-width: 25px; 205 | } 206 | 207 | .music-number:after { 208 | content: '.'; 209 | } 210 | 211 | .music-title { 212 | color: rgba(255, 255, 255, 0.81); 213 | width: 100%; 214 | } 215 | 216 | .music-duration { 217 | text-align: right; 218 | width: 100%; 219 | } 220 | -------------------------------------------------------------------------------- /tests/AlbumList.spec.js: -------------------------------------------------------------------------------- 1 | import 'jsdom-global/register'; 2 | import { expect } from 'chai'; 3 | import renderAlbums from '../src/AlbumList'; 4 | 5 | describe('AlbumList', () => { 6 | it('should exist', () => { 7 | expect(renderAlbums).to.exist; 8 | }); 9 | 10 | const data = [ 11 | { 12 | "album_type" : "album", 13 | "artists" : [ { 14 | "name" : "Incubus", 15 | } ], 16 | "id" : "6peEdPVO73WtgGah5sEhX4", 17 | "images" : [ { 18 | "height" : 640, 19 | "url" : "https://i.scdn.co/image/59a536f0bf0ddaa427e4c732a061c33fe7578757", 20 | "width" : 640 21 | }, { 22 | "height" : 300, 23 | "url" : "https://i.scdn.co/image/9d6866c93e476bd8e7aa7771f9b68db119e076c6", 24 | "width" : 300 25 | }, { 26 | "height" : 64, 27 | "url" : "https://i.scdn.co/image/3ad2701e3f6fe51404f3a4de7a5b2c7b745bad16", 28 | "width" : 64 29 | } ], 30 | "name" : "The Essential Incubus", 31 | "type" : "album", 32 | } 33 | ]; 34 | 35 | const data2 = [ 36 | { 37 | "album_type" : "album", 38 | "artists" : [ { 39 | "name" : "Incubus", 40 | } ], 41 | "id" : "6peEdPVO73WtgGah5sEhX4", 42 | "images" : [ { 43 | "height" : 640, 44 | "url" : "https://i.scdn.co/image/59a536f0bf0ddaa427e4c732a061c33fe7578757", 45 | "width" : 640 46 | }, { 47 | "height" : 300, 48 | "url" : "https://i.scdn.co/image/9d6866c93e476bd8e7aa7771f9b68db119e076c6", 49 | "width" : 300 50 | }, { 51 | "height" : 64, 52 | "url" : "https://i.scdn.co/image/3ad2701e3f6fe51404f3a4de7a5b2c7b745bad16", 53 | "width" : 64 54 | } ], 55 | "name" : "The Essential Incubus", 56 | "type" : "album", 57 | }, 58 | { 59 | "album_type" : "album", 60 | "artists" : [ { 61 | "name" : "Incubus", 62 | } ], 63 | "id" : "6peEdPVO73WtgGah5sEhX4", 64 | "images" : [ { 65 | "height" : 640, 66 | "url" : "https://i.scdn.co/image/59a536f0bf0ddaa427e4c732a061c33fe7578757", 67 | "width" : 640 68 | }, { 69 | "height" : 300, 70 | "url" : "https://i.scdn.co/image/9d6866c93e476bd8e7aa7771f9b68db119e076c6", 71 | "width" : 300 72 | }, { 73 | "height" : 64, 74 | "url" : "https://i.scdn.co/image/3ad2701e3f6fe51404f3a4de7a5b2c7b745bad16", 75 | "width" : 64 76 | } ], 77 | "name" : "The Essential Incubus", 78 | "type" : "album", 79 | } 80 | ]; 81 | 82 | const markup = ` 83 |
84 | ${data[0].name} 85 |
86 |

${data[0].name}

87 |

${data[0].artists[0].name}

88 |
89 |
`; 90 | 91 | const markup2 = ` 92 |
93 | ${data[0].name} 94 |
95 |

${data[0].name}

96 |

${data[0].artists[0].name}

97 |
98 |
99 |
100 | ${data[0].name} 101 |
102 |

${data[0].name}

103 |

${data[0].artists[0].name}

104 |
105 |
`; 106 | 107 | it('should create and append the markup given a correct data', () => { 108 | const element = document.createElement('div'); 109 | renderAlbums(data, element); 110 | 111 | expect(element.innerHTML).to.be.eql(markup); 112 | }); 113 | 114 | it('should create and append when more than 1 album', () => { 115 | const element2 = document.createElement('div'); 116 | renderAlbums(data2, element2); 117 | 118 | expect(element2.innerHTML).to.be.eql(markup2); 119 | }); 120 | 121 | }); 122 | --------------------------------------------------------------------------------