├── .gitignore
├── README.md
├── TASKS.md
├── babel.config.json
├── craco.config.js
├── jsconfig.json
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
└── src
├── AudioControls
├── AudioControls.js
└── AudioControls.scss
├── AudioPlayer.scss
├── AudioProgress
├── AudioProgress.js
└── AudioProgress.scss
├── AudioVolume
├── AudioVolume.js
└── AudioVolume.scss
├── PlayerIcon
├── Icons
│ ├── Backward.js
│ ├── Forward.js
│ ├── Pause.js
│ ├── Play.js
│ ├── Random.js
│ ├── Sync.js
│ ├── VolumeDown.js
│ ├── VolumeOff.js
│ └── VolumeUp.js
├── PlayerIcon.js
└── PlayerIcon.scss
├── SongInfo
├── SongInfo.js
└── SongInfo.scss
├── index.js
├── store
├── actions
│ ├── setAudio.js
│ ├── setControls.js
│ └── setProgress.js
├── reactions.js
└── store.js
├── styles
├── _base.scss
├── _mixins.scss
├── _normalize.scss
├── _variables.scss
└── index.scss
└── utils
├── formatTime.js
└── objectUtils.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 | dist
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > https://www.patreon.com/champipatreon
2 |
3 | # react-playlist-player
4 |
5 | [Open live demo](https://react-playlist-player.firebaseapp.com/)
6 |
7 | ## Install
8 |
9 | ```javascript
10 | npm install react-playlist-player mobx mobx-react --save
11 | ```
12 |
13 | You'll also need the following devDependencies:
14 |
15 | ```json
16 | "devDependencies": {
17 | "@babel/cli": "^7.14.5",
18 | "@babel/core": "^7.14.6",
19 | "@babel/plugin-proposal-decorators": "^7.14.5",
20 | "@babel/plugin-syntax-jsx": "^7.14.5",
21 | "@babel/polyfill": "^7.12.1",
22 | "@babel/preset-env": "^7.14.7",
23 | "@craco/craco": "^5.6.4",
24 | "node-sass": "^6.0.1"
25 | }
26 | ```
27 |
28 | then update the scripts:
29 |
30 | ```json
31 | "scripts": {
32 | "start": "craco start",
33 | "build": "craco build"
34 | }
35 | ```
36 |
37 | and add a craco.config.js at the root of your project:
38 |
39 | ```javascript
40 | module.exports = {
41 | reactScriptsVersion: "react-scripts",
42 | babel: {
43 | plugins: [["@babel/plugin-proposal-decorators", { legacy: true }]]
44 | }
45 | };
46 | ```
47 |
48 | ## Usage
49 |
50 | ```javascript
51 | import React, { Component } from 'react'
52 | import { render } from 'react-dom'
53 | import AudioPlayer from 'react-playlist-player'
54 |
55 | class Demo extends Component {
56 | state = {
57 | currentPlayList: {}
58 | }
59 |
60 | loadPlayList = () =>
61 | this.setState({
62 | currentPlayList: {
63 | playlistCoverUrl: 'path/to/coverUrl',
64 | playlistName: 'playlist name',
65 | bandName: 'band name',
66 | songs: [
67 | {
68 | position: '1',
69 | songName: 'foo',
70 | songUrl: 'path/to/songUrl'
71 | },
72 | {
73 | position: '2',
74 | songName: 'bar',
75 | songUrl: 'path/to/songUrl'
76 | },
77 | {
78 | position: '3',
79 | songName: 'baz',
80 | songUrl: 'path/to/songUrl'
81 | }
82 | ],
83 | type: 'album'
84 | }
85 | })
86 |
87 | render() {
88 | return (
89 |
90 |
93 |
console.log({audioPlaying})}
95 | onSongChanged={({currentSong}) => {console.log(currentSong)}}
96 | />
97 |
98 | )
99 | }
100 | }
101 |
102 | render(, document.querySelector('#demo'))
103 | ```
104 |
105 | ## Props
106 |
107 | | Prop | Type | Required | Description |
108 | | --------------- | :----: | -------- | -------------------------------------------------------------- |
109 | | onToggle | Function | | A function to be excuted on audio toggle. It'll get passed {audioPlaying} as an argument |
110 | | onSongChanged | Function | | A function that is called when a song changes, receives {currentSong} as param |
111 | | currentPlayList | Object | * | An object containing the playlist data |
112 | | playlistCoverUrl | String | * | A path to the cover image (prop of currentPlayList) |
113 | | playlistName | String | * | Playlist name (prop of currentPlayList) |
114 | | bandName | String | * | Band name (prop of currentPlayList) |
115 | | songs | Array | * | Array of songs(objects) to be played (prop of currentPlayList) |
116 | | position | String | | Song's position in playlist (prop of songs) |
117 | | songName | String | * | Song name (prop of songs) |
118 | | songUrl | String | * | A path to the song (prop of songs) |
119 |
120 | ## Exposed api
121 |
122 | ### toggleAudio
123 |
124 | ```javascript
125 | import { toggleAudio } from 'react-playlist-player'
126 |
127 | // Plays / pauses the audio
128 | toggleAudio()
129 | ```
130 |
--------------------------------------------------------------------------------
/TASKS.md:
--------------------------------------------------------------------------------
1 | [TODO]:
2 | - [x] Expose a variable/getter that holds the index of the song that's being played
3 | - [x] Adjust/review npm build and publish steps
--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/env",
5 | {
6 | "targets": {
7 | "edge": "17",
8 | "firefox": "60",
9 | "chrome": "67",
10 | "safari": "11.1"
11 | },
12 | "useBuiltIns": "usage",
13 | "corejs": "3.6.5"
14 | }
15 | ],
16 | "@babel/preset-react"
17 | ],
18 | "plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }]]
19 | }
--------------------------------------------------------------------------------
/craco.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | reactScriptsVersion: "react-scripts",
3 | babel: {
4 | plugins: [["@babel/plugin-proposal-decorators", { legacy: true }]]
5 | }
6 | };
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "experimentalDecorators": true
6 | },
7 | "exclude": ["node_modules"]
8 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-playlist-player",
3 | "version": "1.2.6",
4 | "description": "A React component for playing playlists",
5 | "author": "daniel sarmiento cordero",
6 | "email": "champi@champi.io",
7 | "license": "MIT",
8 | "keywords": [
9 | "react",
10 | "components",
11 | "ui",
12 | "music"
13 | ],
14 | "main": "dist/index.js",
15 | "module": "dist/index.js",
16 | "files": [
17 | "dist",
18 | "README.md"
19 | ],
20 | "repository": {
21 | "type": "git",
22 | "url": "git+https://github.com/champi-dev/react-playlist-player"
23 | },
24 | "private": false,
25 | "dependencies": {
26 | "mobx": "^5.15.4",
27 | "mobx-react": "^6.2.2",
28 | "react": "^16.13.1",
29 | "react-dom": "^16.13.1",
30 | "react-scripts": "^4.0.3"
31 | },
32 | "scripts": {
33 | "start": "craco start",
34 | "build": "rm -rf dist && NODE_ENV=production babel src/ --out-dir dist --copy-files"
35 | },
36 | "eslintConfig": {
37 | "extends": "react-app"
38 | },
39 | "browserslist": {
40 | "production": [
41 | ">0.2%",
42 | "not dead",
43 | "not op_mini all"
44 | ],
45 | "development": [
46 | "last 1 chrome version",
47 | "last 1 firefox version",
48 | "last 1 safari version"
49 | ]
50 | },
51 | "devDependencies": {
52 | "@babel/plugin-proposal-decorators": "^7.14.5",
53 | "@craco/craco": "^5.6.4",
54 | "node-sass": "^6.0.1",
55 | "@babel/cli": "^7.14.5",
56 | "@babel/core": "^7.14.6",
57 | "@babel/polyfill": "^7.12.1",
58 | "@babel/preset-env": "^7.14.7"
59 | }
60 | }
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/champi-dev/react-playlist-player/829ae4e45ed17d29fc0fe03be533f9ab66b0578c/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/champi-dev/react-playlist-player/829ae4e45ed17d29fc0fe03be533f9ab66b0578c/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/champi-dev/react-playlist-player/829ae4e45ed17d29fc0fe03be533f9ab66b0578c/public/logo512.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/AudioControls/AudioControls.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { observer } from 'mobx-react'
3 | import { objectHasProps } from '../utils/objectUtils'
4 | import store from '../store/store'
5 | import './AudioControls.scss'
6 | import PlayerIcon from '../PlayerIcon/PlayerIcon'
7 | import AudioVolume from '../AudioVolume/AudioVolume'
8 |
9 | export const randomClasses = ({ randomize }) => (randomize ? 'random active' : 'random')
10 | export const playClasses = ({ audioPlaying }) => (audioPlaying ? 'pause' : 'play')
11 | export const repeatClasses = ({ repeat }) => {
12 | switch (repeat) {
13 | case 'off':
14 | return 'sync'
15 | case 'all':
16 | return 'sync active'
17 | case 'one':
18 | return 'sync active active--twice'
19 | default:
20 | break
21 | }
22 | }
23 |
24 | const toggleHandler = () => {
25 | if (objectHasProps(store.state.currentPlayList)) {
26 | store.setAudio().toggle()
27 | }
28 | }
29 |
30 | const AudioControls = observer((props) => (
31 |
32 |
store.setControls().toggleRandomize()}
36 | />
37 | store.setControls().skipBackward()} />
38 |
39 | toggleHandler(props)}
43 | />
44 |
45 | store.setControls().skipForward()} />
46 | store.setControls().toggleRepeat()} />
47 |
48 |
49 | ))
50 |
51 | export default AudioControls
52 |
--------------------------------------------------------------------------------
/src/AudioControls/AudioControls.scss:
--------------------------------------------------------------------------------
1 | @import '../styles/_variables.scss';
2 |
3 | .icons {
4 | position: absolute;
5 | top: 50%;
6 | left: 50%;
7 | transform: translate(-50%, -50%);
8 | @media screen and (min-width: $mediumBreakPoint) {
9 | position: unset;
10 | top: unset;
11 | left: unset;
12 | transform: unset;
13 | text-align: center;
14 | margin-bottom: 1rem;
15 | }
16 | }
--------------------------------------------------------------------------------
/src/AudioPlayer.scss:
--------------------------------------------------------------------------------
1 | @import "./styles/_variables.scss";
2 | $player-bg: lighten($bg-primary, 5%);
3 |
4 | .audio__player {
5 | position: fixed;
6 | bottom: 0;
7 | left: 0;
8 | width: 100%;
9 | background-color: $player-bg;
10 |
11 | & .audio__controls {
12 | position: relative;
13 | height: 2.5rem;
14 | @media screen and (min-width: $mediumBreakPoint) {
15 | height: 5rem;
16 | }
17 | }
18 |
19 | & .group {
20 | @media screen and (min-width: $mediumBreakPoint) {
21 | position: absolute;
22 | width: 100%;
23 | top: 50%;
24 | left: 50%;
25 | transform: translate(-50%, -50%);
26 | }
27 |
28 | & .play,
29 | & .pause {
30 | svg {
31 | width: 1.7rem;
32 | height: 1.7rem;
33 | transition: transform $transition-primary;
34 | @media screen and (min-width: $mediumBreakPoint) {
35 | &:hover {
36 | transform: scale(1.1);
37 | }
38 | }
39 | }
40 | }
41 | & .backward,
42 | & .forward {
43 | svg {
44 | width: 1rem;
45 | height: 1rem;
46 | }
47 | }
48 |
49 | & .random,
50 | & .sync {
51 | svg {
52 | width: 0.8rem;
53 | height: 0.8rem;
54 | }
55 | }
56 |
57 | & .volumeup,
58 | & .volumedown,
59 | & .volumeoff {
60 | svg {
61 | width: 1rem;
62 | height: 1rem;
63 | }
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/AudioProgress/AudioProgress.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { observer } from 'mobx-react'
3 | import store from '../store/store'
4 | import './AudioProgress.scss'
5 | import { formatTime } from '../utils/formatTime'
6 |
7 | export const seekPlaying = (e, store, testConfig = {}) => {
8 | if (!store.canPlay) return undefined
9 |
10 | const desiredPos = e.clientX
11 | const playerWidth = getPlayerWidth(testConfig)
12 | const progressEl = getProgressEl(testConfig)
13 | const progressWidth = progressEl.offsetWidth
14 | const progressOffsetLeft = progressEl.offsetLeft
15 | let positionToSet = 0
16 |
17 | if (playerWidth === progressWidth) {
18 | positionToSet = Math.round(
19 | (desiredPos / progressWidth) * store.setAudio().getDuration()
20 | )
21 | } else if (playerWidth > progressWidth) {
22 | positionToSet = Math.round(
23 | ((desiredPos - progressOffsetLeft) / progressWidth) *
24 | store.setAudio().getDuration()
25 | )
26 | }
27 |
28 | return positionToSet
29 | }
30 |
31 | const getPlayerWidth = ({ player }) => {
32 | if (player) return player.width
33 | return document.getElementById('audio__player').offsetWidth
34 | }
35 |
36 | const getProgressEl = ({ progress }) => {
37 | if (progress)
38 | return { offsetWidth: progress.width, offsetLeft: progress.left }
39 | return document.getElementById('progress')
40 | }
41 |
42 | const AudioProgress = observer(() => {
43 | return (
44 | store.setAudio().setCurrentTime(seekPlaying(e, store))}
48 | >
49 |
50 | {formatTime(store.setAudio().getCurrentTime())}
51 |
52 |
56 |
57 | {formatTime(store.setAudio().getDuration())}
58 |
59 |
60 | )
61 | })
62 |
63 | export default AudioProgress
64 |
--------------------------------------------------------------------------------
/src/AudioProgress/AudioProgress.scss:
--------------------------------------------------------------------------------
1 | @import '../styles/_variables.scss';
2 | @import '../styles/_mixins.scss';
3 |
4 | .progress {
5 | backface-visibility: hidden;
6 | position: absolute;
7 | width: 100%;
8 | height: 2px;
9 | top: -2px;
10 | background: lighten($bg-primary, 15%);
11 | @media screen and (min-width: $mediumBreakPoint) {
12 | position: relative;
13 | width: 35%;
14 | max-width: 40rem;
15 | margin: 0 auto;
16 | height: 4px;
17 | border-radius: 4px;
18 | &:hover {
19 | .progress__fill {
20 | background-color: $primary;
21 | }
22 | }
23 | }
24 |
25 | &__fill {
26 | @include bar-fill;
27 | }
28 |
29 | & .time {
30 | user-select: none;
31 | cursor: default;
32 | position: absolute;
33 | top: 50%;
34 | transform: translateY(-50%);
35 | color: $text-secondary;
36 | font-size: 0.6rem;
37 | &.current-time {
38 | left: -1.5rem;
39 | }
40 | &.total-time {
41 | right: -1.5rem;
42 | }
43 | }
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/src/AudioVolume/AudioVolume.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { observer } from 'mobx-react'
3 | import store from '../store/store'
4 | import './AudioVolume.scss'
5 | import PlayerIcon from '../PlayerIcon/PlayerIcon'
6 |
7 | export const volumeClass = state => {
8 | if (state.audioVolume >= 0.5) {
9 | return 'volumeup'
10 | } else if (state.audioVolume < 0.5 && state.audioVolume > 0) {
11 | return 'volumedown'
12 | } else {
13 | return 'volumeoff'
14 | }
15 | }
16 |
17 | export const toggleVolume = state => (state.audioVolume > 0.0 ? 0.0 : 1.0)
18 |
19 | export const seekVolume = (e, testConfig = {}) => {
20 | const desiredPos = e.clientX
21 | const volumeOffsetLeft = getVolumeOffsetLeft(testConfig)
22 | const volumeBarOffsetLeft = getVolumeBarOffsetLeft(testConfig)
23 | const volumeBarWidth = getVolumeBarWidth(testConfig)
24 |
25 | return Math.round(((desiredPos - volumeOffsetLeft - volumeBarOffsetLeft) / volumeBarWidth) * 10) / 10
26 | }
27 |
28 | const getVolumeOffsetLeft = ({ volume }) => {
29 | if (volume) return volume.offsetLeft
30 | return document.getElementById('audio-volume').offsetLeft
31 | }
32 |
33 | const getVolumeBarOffsetLeft = ({ volumeBar }) => {
34 | if (volumeBar) return volumeBar.offsetLeft
35 | return document.getElementsByClassName('audio-volume__level')[0].offsetLeft
36 | }
37 |
38 | const getVolumeBarWidth = ({ volumeBar }) => {
39 | if (volumeBar) return volumeBar.width
40 | return document.getElementsByClassName('audio-volume__level')[0].offsetWidth
41 | }
42 |
43 | const AudioVolume = observer(() => {
44 | return (
45 |
46 |
50 | store
51 | .setAudio()
52 | .setVolume(toggleVolume(store.state))
53 | .visual()
54 | }
55 | />
56 |
59 | store
60 | .setAudio()
61 | .setVolume(seekVolume(e))
62 | .visual()
63 | }
64 | >
65 |
66 |
67 |
68 | )
69 | })
70 |
71 | export default AudioVolume
72 |
--------------------------------------------------------------------------------
/src/AudioVolume/AudioVolume.scss:
--------------------------------------------------------------------------------
1 | @import '../styles/_variables.scss';
2 | @import '../styles/_mixins.scss';
3 |
4 | .audio-volume {
5 | display: none;
6 | position: absolute;
7 | top: 50%;
8 | right: $horizontalSpace;
9 | transform: translateY(-50%);
10 | @media screen and (min-width: $mediumBreakPoint) {
11 | display: block;
12 | right: $horizontalSpaceLarge;
13 | }
14 |
15 | &__level {
16 | width: 4rem;
17 | height: 4px;
18 | border-radius: 4px;
19 | background-color: lighten($bg-primary, 15%);
20 | display: inline-block;
21 | vertical-align: middle;
22 | &:hover {
23 | .audio-volume__fill {
24 | background-color: $primary;
25 | }
26 | }
27 | }
28 |
29 | &__fill {
30 | @include bar-fill;
31 | }
32 |
33 | .icon {
34 | position: relative;
35 | display: inline-block;
36 | vertical-align: middle;
37 | margin-right: 1.3rem;
38 | svg {
39 | position: absolute;
40 | top: 50%;
41 | transform: translateY(-50%);
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/PlayerIcon/Icons/Backward.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const BackWard = props => {
4 | return (
5 |
11 | )
12 | }
13 |
14 | export default BackWard
15 |
--------------------------------------------------------------------------------
/src/PlayerIcon/Icons/Forward.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Forward = props => {
4 | return (
5 |
11 | )
12 | }
13 |
14 | export default Forward
15 |
--------------------------------------------------------------------------------
/src/PlayerIcon/Icons/Pause.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Pause = props => {
4 | return (
5 |
11 | )
12 | }
13 |
14 | export default Pause
15 |
--------------------------------------------------------------------------------
/src/PlayerIcon/Icons/Play.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Play = props => {
4 | return (
5 |
11 | )
12 | }
13 |
14 | export default Play
15 |
--------------------------------------------------------------------------------
/src/PlayerIcon/Icons/Random.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Random = props => {
4 | return (
5 |
11 | )
12 | }
13 |
14 | export default Random
15 |
--------------------------------------------------------------------------------
/src/PlayerIcon/Icons/Sync.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Sync = props => {
4 | return (
5 |
11 | )
12 | }
13 |
14 | export default Sync
15 |
--------------------------------------------------------------------------------
/src/PlayerIcon/Icons/VolumeDown.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const VolumeDown = props => {
4 | return (
5 |
11 | )
12 | }
13 |
14 | export default VolumeDown
15 |
--------------------------------------------------------------------------------
/src/PlayerIcon/Icons/VolumeOff.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const VolumeOff = props => {
4 | return (
5 |
11 | )
12 | }
13 |
14 | export default VolumeOff
15 |
--------------------------------------------------------------------------------
/src/PlayerIcon/Icons/VolumeUp.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const VolumeUp = props => {
4 | return (
5 |
11 | )
12 | }
13 |
14 | export default VolumeUp
15 |
--------------------------------------------------------------------------------
/src/PlayerIcon/PlayerIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './PlayerIcon.scss'
3 | import Random from './Icons/Random'
4 | import Sync from './Icons/Sync'
5 | import Play from './Icons/Play'
6 | import Pause from './Icons/Pause'
7 | import Forward from './Icons/Forward'
8 | import Backward from './Icons/Backward'
9 | import VolumeUp from './Icons/VolumeUp'
10 | import VolumeDown from './Icons/VolumeDown'
11 | import VolumeOff from './Icons/VolumeOff'
12 |
13 | export const iconToRender = props => {
14 | const requestedIcon = props.icon
15 |
16 | switch (requestedIcon) {
17 | case 'random':
18 | return
19 | case 'sync':
20 | return
21 | case 'play':
22 | return
23 | case 'pause':
24 | return
25 | case 'forward':
26 | return
27 | case 'backward':
28 | return
29 | case 'volumeup':
30 | return
31 | case 'volumedown':
32 | return
33 | case 'volumeoff':
34 | return
35 | default:
36 | break
37 | }
38 | }
39 |
40 | const PlayerIcon = props => {
41 | return (
42 |
43 | {iconToRender(props)}
44 |
45 | )
46 | }
47 |
48 | export default PlayerIcon
49 |
--------------------------------------------------------------------------------
/src/PlayerIcon/PlayerIcon.scss:
--------------------------------------------------------------------------------
1 | @import "../styles/_variables.scss";
2 |
3 | .icon {
4 | position: relative;
5 | display: inline-block;
6 | vertical-align: middle;
7 | transition: transform $transition-primary;
8 | backface-visibility: hidden;
9 | &:not(:nth-child(5)) {
10 | margin-right: 1rem;
11 | }
12 | &:nth-child(1),
13 | &:nth-child(4) {
14 | margin-right: 1.3rem;
15 | }
16 |
17 | &:hover {
18 | #iconPath {
19 | fill: $text;
20 | }
21 | }
22 | &:active {
23 | #iconPath {
24 | fill: darken($text-secondary, 5%);
25 | }
26 | }
27 |
28 | #iconPath {
29 | fill: $text-secondary;
30 | }
31 |
32 | &.active {
33 | #iconPath {
34 | fill: $primary;
35 | }
36 | &::after {
37 | position: absolute;
38 | bottom: -8px;
39 | left: 50%;
40 | transform: translateX(-50%);
41 | content: "";
42 | width: 4px;
43 | height: 4px;
44 | background: $primary;
45 | display: block;
46 | border-radius: 50%;
47 | }
48 | &.active--twice {
49 | &::before {
50 | content: "1";
51 | position: absolute;
52 | top: 0px;
53 | right: -10px;
54 | font-size: 8px;
55 | -webkit-transform: translateX(-50%);
56 | transform: translateX(-50%);
57 | width: 10px;
58 | background: $primary;
59 | display: block;
60 | border-radius: 50%;
61 | }
62 | }
63 | }
64 |
65 | svg {
66 | display: inline-block;
67 | vertical-align: middle;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/SongInfo/SongInfo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { observer } from 'mobx-react'
3 | import store from '../store/store'
4 | import './SongInfo.scss'
5 |
6 | const SongInfo = observer(() => {
7 | return (
8 |
9 |

10 |
11 | {store.state.currentSong.songName}
12 | {store.state.currentPlayList.bandName}
13 |
14 |
15 | )
16 | })
17 |
18 | export default SongInfo
19 |
--------------------------------------------------------------------------------
/src/SongInfo/SongInfo.scss:
--------------------------------------------------------------------------------
1 | @import "../styles/_variables.scss";
2 |
3 | .song-info {
4 | z-index: 2;
5 | display: none;
6 | position: absolute;
7 | top: 50%;
8 | left: $horizontalSpace;
9 | transform: translateY(-50%);
10 | @media screen and (min-width: $mediumBreakPoint) {
11 | display: block;
12 | left: $horizontalSpaceLarge;
13 | }
14 | &__cover {
15 | height: 3.5rem;
16 | width: 3.5rem;
17 | box-shadow: $shadowMainBox;
18 | display: inline-block;
19 | vertical-align: middle;
20 | object-fit: cover;
21 | }
22 | &__text {
23 | display: inline-block;
24 | vertical-align: middle;
25 | padding-left: 0.5rem;
26 | & .title,
27 | & .subtitle {
28 | cursor: default;
29 | display: block;
30 | text-transform: capitalize;
31 | &:hover {
32 | color: $text;
33 | text-decoration: underline;
34 | }
35 | }
36 | & .title {
37 | color: $text;
38 | font-size: 0.8rem;
39 | line-height: 1.7;
40 | }
41 | & .subtitle {
42 | color: $text-secondary;
43 | font-size: 0.65rem;
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { observer } from 'mobx-react'
3 | import store from './store/store'
4 | import './AudioPlayer.scss'
5 | import SongInfo from './SongInfo/SongInfo'
6 | import AudioControls from './AudioControls/AudioControls'
7 | import AudioProgress from './AudioProgress/AudioProgress'
8 | import isEqual from 'lodash/isEqual'
9 | import { testCond } from './utils/objectUtils'
10 |
11 | export const exportProps = {
12 | onToggle: () => {},
13 | onSongChanged: () => {}
14 | }
15 |
16 | @observer
17 | class AudioPlayer extends Component {
18 | componentDidMount() {
19 | const {onToggle} = this.props
20 | if (onToggle) exportProps.onToggle = onToggle
21 |
22 | const {onSongChanged} = this.props
23 | if (onSongChanged) exportProps.onSongChanged = onSongChanged
24 | }
25 |
26 | componentDidUpdate(prevProps) {
27 | this.onPlaylistChanged(prevProps)
28 | }
29 |
30 | onPlaylistChanged = (prevProps, testConfig = {}) => {
31 | if (
32 | testCond(
33 | testConfig,
34 | 'ifBool',
35 | !isEqual(prevProps.currentPlayList, this.props.currentPlayList)
36 | )
37 | ) {
38 | store.setAudio().setPlaylist(this.props.currentPlayList)
39 | }
40 | }
41 |
42 | render() {
43 | const SongInfoComponent = store.canPlay &&
44 | const AudioElement = (
45 |
52 | )
53 |
54 | return (
55 |
56 |
57 | {AudioElement}
58 |
59 | {SongInfoComponent}
60 |
64 |
65 |
66 |
67 | )
68 | }
69 | }
70 |
71 | export const toggleAudio = store.setAudio().toggle
72 | export default AudioPlayer
73 |
--------------------------------------------------------------------------------
/src/store/actions/setAudio.js:
--------------------------------------------------------------------------------
1 | import { objectHasProps } from '../../utils/objectUtils'
2 | import { exportProps } from '../../index'
3 |
4 | const setAudio = () =>
5 | function() {
6 | return {
7 | toggle: ({ shouldLoad } = {}) => {
8 | if (this.state.audioElement) {
9 | if (shouldLoad) this.setAudio().load()
10 |
11 | this.state.audioPlaying
12 | ? this.setAudio().pause()
13 | : this.setAudio().play()
14 |
15 | exportProps.onToggle({audioPlaying: this.state.audioPlaying})
16 | }
17 | },
18 |
19 | play: () => {
20 | this.state.audioElement.play()
21 | this.state.audioPlaying = true
22 | },
23 |
24 | pause: () => {
25 | this.state.audioElement.pause()
26 | this.state.audioPlaying = false
27 | },
28 |
29 | resetPlay: () => {
30 | this.setAudio().toggle()
31 | this.setAudio().setCurrentTime(0)
32 | this.setProgress().set('0%')
33 | },
34 |
35 | playFromTop: ({ auto } = {}) => {
36 | this.state.playedIndexes = []
37 | this.setAudio().resetPlay()
38 | this.setAudio().setSong({ arrIndex: 0 })
39 | if (auto) this.setAudio().setAndPlay({ shouldLoad: true })
40 | },
41 |
42 | load: () => this.state.audioElement.load(),
43 |
44 | getVolume: () => this.state.audioElement.volume,
45 |
46 | getDuration: () =>
47 | this.state.audioElement ? this.state.audioElement.duration : undefined,
48 |
49 | getCurrentTime: () =>
50 | this.state.audioElement
51 | ? this.state.audioElement.currentTime
52 | : undefined,
53 |
54 | setCurrentTime: pos =>
55 | this.state.audioElement
56 | ? (this.state.audioElement.currentTime = pos)
57 | : '',
58 |
59 | setElement: () =>
60 | Promise.resolve(
61 | (this.state.audioElement = document.getElementById('audio'))
62 | ),
63 |
64 | setPlaylist: playlist => (this.state.currentPlayList = { ...playlist }),
65 |
66 | setSong: ({ arrIndex }) =>
67 | objectHasProps(this.state.currentPlayList) &&
68 | (this.state.currentSong = {
69 | ...this.state.currentPlayList.songs[arrIndex],
70 | arrIndex
71 | }),
72 |
73 | setSongBy: by => {
74 | this.setAudio().setSong({
75 | arrIndex: this.state.currentSong.arrIndex + by
76 | })
77 | },
78 |
79 | setVolume: volume => ({
80 | visual: () => (this.state.audioVolume = volume),
81 | element: () => (this.state.audioElement.volume = volume)
82 | }),
83 |
84 | setAndPlay: ({ shouldLoad } = {}) => {
85 | const saveIndex = () => {
86 | const foundIndex = this.state.playedIndexes.find(
87 | i => i === this.state.currentSong.arrIndex
88 | )
89 | if (foundIndex === undefined)
90 | this.state.playedIndexes.push(this.state.currentSong.arrIndex)
91 | }
92 |
93 | if (this.canPlay) {
94 | this.setAudio()
95 | .setElement()
96 | .then(() => {
97 | saveIndex()
98 | this.setAudio().toggle({ shouldLoad })
99 | })
100 | .catch(e => e)
101 | }
102 | }
103 | }
104 | }
105 |
106 | export default setAudio
107 |
--------------------------------------------------------------------------------
/src/store/actions/setControls.js:
--------------------------------------------------------------------------------
1 | import { exportProps } from '../../index'
2 |
3 | const setControls = () =>
4 | function() {
5 | return {
6 | toggleRandomize: () => {
7 | this.state.randomize = !this.state.randomize
8 | },
9 |
10 | toggleRepeat: () => {
11 | const setRepeat = repeat => {
12 | this.state.repeat = repeat
13 | }
14 |
15 | switch (this.state.repeat) {
16 | case 'off':
17 | setRepeat('all')
18 | break
19 | case 'all':
20 | setRepeat('one')
21 | break
22 | case 'one':
23 | setRepeat('off')
24 | break
25 | default:
26 | break
27 | }
28 | },
29 |
30 | skipBackward: () => {
31 | if (this.state.playedIndexes.length > 1) {
32 | this.state.backwardTimes += 1
33 | this.setControls().skipSong({
34 | to: this.state.playedIndexes[this.state.playedIndexes.length - this.state.backwardTimes - 1]
35 | })
36 | if (this.state.backwardTimes === this.state.playedIndexes.length - 1) {
37 | this.state.backwardTimes = 0
38 | this.state.playedIndexes = []
39 | }
40 | }
41 | },
42 |
43 | skipForward: () => {
44 | if (this.state.randomize) {
45 | const index = this.setControls().findRandomIndex()
46 | if (!index && this.setControls().playedAllSongs()) {
47 | this.state.repeat === 'all'
48 | ? this.setAudio().playFromTop({ auto: true })
49 | : this.setAudio().playFromTop({ auto: null })
50 | } else {
51 | this.setControls().skipSong({ to: index })
52 | }
53 | } else {
54 | this.setControls().skipSong({ by: 1 })
55 | }
56 | },
57 |
58 | skipSong: ({ by, to } = {}) => {
59 | const skipper = skipFn => {
60 | this.setAudio().resetPlay()
61 | skipFn()
62 | this.setAudio().setAndPlay({ shouldLoad: true })
63 | exportProps.onSongChanged({currentSong: this.state.currentSong})
64 | }
65 |
66 | if (this.songAndPlaylistAreSetted) {
67 | if (this.state.repeat === 'all' && this.setControls().playedAllSongs() && by > 0) {
68 | this.setAudio().playFromTop({ auto: true })
69 | return
70 | }
71 | if (typeof by === 'number') {
72 | const nextIndex = this.state.currentSong.arrIndex + by
73 | if (nextIndex >= 0 && nextIndex < this.state.currentPlayList.songs.length) {
74 | skipper(() => this.setAudio().setSongBy(by))
75 | }
76 | } else if (typeof to === 'number' && to >= 0) {
77 | skipper(() => this.setAudio().setSong({ songs: this.state.currentPlayList.songs, arrIndex: to }))
78 | }
79 | }
80 | },
81 |
82 | findRandomIndex: () => {
83 | const plLength = this.state.currentPlayList.songs.length - 1
84 | const randomIndex = () => Math.round((Math.random() * plLength * 10) / 10)
85 | const generatedIndex = randomIndex()
86 |
87 | if (this.state.playedIndexes.find(i => i === generatedIndex) === undefined) {
88 | return generatedIndex
89 | } else if (this.setControls().playedAllSongs()) {
90 | return
91 | }
92 | return this.setControls().findRandomIndex()
93 | },
94 |
95 | playedAllSongs: () => this.state.currentPlayList.songs.length === this.state.playedIndexes.length
96 | }
97 | }
98 |
99 | export default setControls
100 |
--------------------------------------------------------------------------------
/src/store/actions/setProgress.js:
--------------------------------------------------------------------------------
1 | const setProgress = () =>
2 | function() {
3 | return {
4 | completed: () => this.state.audioProgress === '100%',
5 |
6 | get: () => this.state.audioProgress,
7 |
8 | set: manualProgress => {
9 | this.state.audioProgress =
10 | manualProgress ||
11 | `${Math.round((this.setAudio().getCurrentTime() / this.setAudio().getDuration()) * 100 * 10) / 10}%`
12 | }
13 | }
14 | }
15 |
16 | export default setProgress
17 |
--------------------------------------------------------------------------------
/src/store/reactions.js:
--------------------------------------------------------------------------------
1 | const reactions = store => [
2 | {
3 | data: () => store.state.currentPlayList,
4 | effect: () => {
5 | store.setAudio().playFromTop({ auto: true })
6 | }
7 | },
8 |
9 | {
10 | data: () => store.state.repeat === 'one' && store.setProgress().completed(),
11 | effect: shouldRepeatOne =>
12 | shouldRepeatOne && store.setControls().skipSong({ by: 0 })
13 | },
14 |
15 | {
16 | data: () =>
17 | store.state.repeat === 'all' &&
18 | store.setProgress().completed() &&
19 | store.setControls().playedAllSongs(),
20 | effect: shouldRepeatPlayList =>
21 | shouldRepeatPlayList && store.setAudio().playFromTop({ auto: true })
22 | },
23 |
24 | {
25 | data: () => store.state.randomize && store.setProgress().completed(),
26 | effect: shouldPlayRandom => {
27 | if (shouldPlayRandom) {
28 | const index = store.setControls().findRandomIndex()
29 | index && store.setControls().skipSong({ to: index })
30 | }
31 | }
32 | },
33 |
34 | {
35 | data: () => store.setProgress().completed() && store.state.repeat === 'off',
36 | effect: shouldPlayNextSong => {
37 | if (shouldPlayNextSong) {
38 | if (
39 | store.state.currentSong.arrIndex + 1 <
40 | store.state.currentPlayList.songs.length
41 | ) {
42 | store.setControls().skipSong({ by: 1 })
43 | } else {
44 | store.setAudio().resetPlay()
45 | }
46 | }
47 | }
48 | },
49 |
50 | {
51 | data: () => store.shouldSyncVolume,
52 | effect: ({ elementIsSetted, volumeVisual }) =>
53 | elementIsSetted &&
54 | store
55 | .setAudio()
56 | .setVolume(volumeVisual)
57 | .element()
58 | }
59 | ]
60 |
61 | export default reactions
62 |
--------------------------------------------------------------------------------
/src/store/store.js:
--------------------------------------------------------------------------------
1 | import { observable, action, computed, reaction } from 'mobx'
2 | import { objectHasProps } from '../utils/objectUtils'
3 | import reactions from './reactions'
4 | import setAudio from './actions/setAudio'
5 | import setControls from './actions/setControls'
6 | import setProgress from './actions/setProgress'
7 |
8 | class Store {
9 | constructor() {
10 | reactions(this).forEach(r => reaction(r.data, r.effect))
11 | }
12 |
13 | @observable state = {
14 | audioElement: undefined,
15 | audioVolume: 1,
16 | audioProgress: '0%',
17 | audioPlaying: false,
18 | randomize: false,
19 | repeat: 'off',
20 | playedIndexes: [],
21 | backwardTimes: 0,
22 | currentSong: {},
23 | currentPlayList: {}
24 | }
25 |
26 | @action setAudio = setAudio().bind(this)
27 | @action setControls = setControls().bind(this)
28 | @action setProgress = setProgress().bind(this)
29 |
30 | @computed get canPlay() {
31 | return objectHasProps(this.state.currentPlayList)
32 | }
33 | @computed get shouldSyncVolume() {
34 | return {
35 | elementIsSetted: !!this.state.audioElement,
36 | volumeVisual: this.state.audioVolume
37 | }
38 | }
39 | @computed get songAndPlaylistAreSetted() {
40 | return (
41 | objectHasProps(this.state.currentSong) &&
42 | objectHasProps(this.state.currentPlayList)
43 | )
44 | }
45 | }
46 |
47 | export default new Store()
48 |
--------------------------------------------------------------------------------
/src/styles/_base.scss:
--------------------------------------------------------------------------------
1 | @import "./_variables.scss";
2 |
3 | * {
4 | padding: 0;
5 | margin: 0;
6 | box-sizing: border-box;
7 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
8 | }
9 |
10 | html {
11 | font-size: 100%;
12 | @media screen and (min-width: $mediumBreakPoint) {
13 | font-size: 115%;
14 | }
15 | }
16 |
17 | body {
18 | margin: 0;
19 | padding: 0;
20 | font-family: "Roboto", sans-serif;
21 | font-weight: 400;
22 | -webkit-font-smoothing: antialiased;
23 | -moz-osx-font-smoothing: grayscale;
24 | }
25 |
--------------------------------------------------------------------------------
/src/styles/_mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin entire-screen {
2 | width: 100vw;
3 | height: 100vh;
4 | }
5 |
6 | @mixin bar-fill {
7 | width: 0%;
8 | height: 100%;
9 | background-color: $text-secondary;
10 | transition: background-color $transition-primary;
11 | backface-visibility: hidden;
12 | @media screen and (min-width: $mediumBreakPoint) {
13 | border-radius: 4px;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/styles/_normalize.scss:
--------------------------------------------------------------------------------
1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
2 |
3 | /* Document
4 | ========================================================================== */
5 |
6 | /**
7 | * 1. Correct the line height in all browsers.
8 | * 2. Prevent adjustments of font size after orientation changes in iOS.
9 | */
10 |
11 | html {
12 | line-height: 1.15; /* 1 */
13 | -webkit-text-size-adjust: 100%; /* 2 */
14 | }
15 |
16 | /* Sections
17 | ========================================================================== */
18 |
19 | /**
20 | * Remove the margin in all browsers.
21 | */
22 |
23 | body {
24 | margin: 0;
25 | }
26 |
27 | /**
28 | * Render the `main` element consistently in IE.
29 | */
30 |
31 | main {
32 | display: block;
33 | }
34 |
35 | /**
36 | * Correct the font size and margin on `h1` elements within `section` and
37 | * `article` contexts in Chrome, Firefox, and Safari.
38 | */
39 |
40 | h1 {
41 | font-size: 2em;
42 | margin: 0.67em 0;
43 | }
44 |
45 | /* Grouping content
46 | ========================================================================== */
47 |
48 | /**
49 | * 1. Add the correct box sizing in Firefox.
50 | * 2. Show the overflow in Edge and IE.
51 | */
52 |
53 | hr {
54 | box-sizing: content-box; /* 1 */
55 | height: 0; /* 1 */
56 | overflow: visible; /* 2 */
57 | }
58 |
59 | /**
60 | * 1. Correct the inheritance and scaling of font size in all browsers.
61 | * 2. Correct the odd `em` font sizing in all browsers.
62 | */
63 |
64 | pre {
65 | font-family: monospace, monospace; /* 1 */
66 | font-size: 1em; /* 2 */
67 | }
68 |
69 | /* Text-level semantics
70 | ========================================================================== */
71 |
72 | /**
73 | * Remove the gray background on active links in IE 10.
74 | */
75 |
76 | a {
77 | background-color: transparent;
78 | }
79 |
80 | /**
81 | * 1. Remove the bottom border in Chrome 57-
82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
83 | */
84 |
85 | abbr[title] {
86 | border-bottom: none; /* 1 */
87 | text-decoration: underline; /* 2 */
88 | text-decoration: underline dotted; /* 2 */
89 | }
90 |
91 | /**
92 | * Add the correct font weight in Chrome, Edge, and Safari.
93 | */
94 |
95 | b,
96 | strong {
97 | font-weight: bolder;
98 | }
99 |
100 | /**
101 | * 1. Correct the inheritance and scaling of font size in all browsers.
102 | * 2. Correct the odd `em` font sizing in all browsers.
103 | */
104 |
105 | code,
106 | kbd,
107 | samp {
108 | font-family: monospace, monospace; /* 1 */
109 | font-size: 1em; /* 2 */
110 | }
111 |
112 | /**
113 | * Add the correct font size in all browsers.
114 | */
115 |
116 | small {
117 | font-size: 80%;
118 | }
119 |
120 | /**
121 | * Prevent `sub` and `sup` elements from affecting the line height in
122 | * all browsers.
123 | */
124 |
125 | sub,
126 | sup {
127 | font-size: 75%;
128 | line-height: 0;
129 | position: relative;
130 | vertical-align: baseline;
131 | }
132 |
133 | sub {
134 | bottom: -0.25em;
135 | }
136 |
137 | sup {
138 | top: -0.5em;
139 | }
140 |
141 | /* Embedded content
142 | ========================================================================== */
143 |
144 | /**
145 | * Remove the border on images inside links in IE 10.
146 | */
147 |
148 | img {
149 | border-style: none;
150 | }
151 |
152 | /* Forms
153 | ========================================================================== */
154 |
155 | /**
156 | * 1. Change the font styles in all browsers.
157 | * 2. Remove the margin in Firefox and Safari.
158 | */
159 |
160 | button,
161 | input,
162 | optgroup,
163 | select,
164 | textarea {
165 | font-family: inherit; /* 1 */
166 | font-size: 100%; /* 1 */
167 | line-height: 1.15; /* 1 */
168 | margin: 0; /* 2 */
169 | }
170 |
171 | /**
172 | * Show the overflow in IE.
173 | * 1. Show the overflow in Edge.
174 | */
175 |
176 | button,
177 | input {
178 | /* 1 */
179 | overflow: visible;
180 | }
181 |
182 | /**
183 | * Remove the inheritance of text transform in Edge, Firefox, and IE.
184 | * 1. Remove the inheritance of text transform in Firefox.
185 | */
186 |
187 | button,
188 | select {
189 | /* 1 */
190 | text-transform: none;
191 | }
192 |
193 | /**
194 | * Correct the inability to style clickable types in iOS and Safari.
195 | */
196 |
197 | button,
198 | [type="button"],
199 | [type="reset"],
200 | [type="submit"] {
201 | -webkit-appearance: button;
202 | }
203 |
204 | /**
205 | * Remove the inner border and padding in Firefox.
206 | */
207 |
208 | button::-moz-focus-inner,
209 | [type="button"]::-moz-focus-inner,
210 | [type="reset"]::-moz-focus-inner,
211 | [type="submit"]::-moz-focus-inner {
212 | border-style: none;
213 | padding: 0;
214 | }
215 |
216 | /**
217 | * Restore the focus styles unset by the previous rule.
218 | */
219 |
220 | button:-moz-focusring,
221 | [type="button"]:-moz-focusring,
222 | [type="reset"]:-moz-focusring,
223 | [type="submit"]:-moz-focusring {
224 | outline: 1px dotted ButtonText;
225 | }
226 |
227 | /**
228 | * Correct the padding in Firefox.
229 | */
230 |
231 | fieldset {
232 | padding: 0.35em 0.75em 0.625em;
233 | }
234 |
235 | /**
236 | * 1. Correct the text wrapping in Edge and IE.
237 | * 2. Correct the color inheritance from `fieldset` elements in IE.
238 | * 3. Remove the padding so developers are not caught out when they zero out
239 | * `fieldset` elements in all browsers.
240 | */
241 |
242 | legend {
243 | box-sizing: border-box; /* 1 */
244 | color: inherit; /* 2 */
245 | display: table; /* 1 */
246 | max-width: 100%; /* 1 */
247 | padding: 0; /* 3 */
248 | white-space: normal; /* 1 */
249 | }
250 |
251 | /**
252 | * Add the correct vertical alignment in Chrome, Firefox, and Opera.
253 | */
254 |
255 | progress {
256 | vertical-align: baseline;
257 | }
258 |
259 | /**
260 | * Remove the default vertical scrollbar in IE 10+.
261 | */
262 |
263 | textarea {
264 | overflow: auto;
265 | }
266 |
267 | /**
268 | * 1. Add the correct box sizing in IE 10.
269 | * 2. Remove the padding in IE 10.
270 | */
271 |
272 | [type="checkbox"],
273 | [type="radio"] {
274 | box-sizing: border-box; /* 1 */
275 | padding: 0; /* 2 */
276 | }
277 |
278 | /**
279 | * Correct the cursor style of increment and decrement buttons in Chrome.
280 | */
281 |
282 | [type="number"]::-webkit-inner-spin-button,
283 | [type="number"]::-webkit-outer-spin-button {
284 | height: auto;
285 | }
286 |
287 | /**
288 | * 1. Correct the odd appearance in Chrome and Safari.
289 | * 2. Correct the outline style in Safari.
290 | */
291 |
292 | [type="search"] {
293 | -webkit-appearance: textfield; /* 1 */
294 | outline-offset: -2px; /* 2 */
295 | }
296 |
297 | /**
298 | * Remove the inner padding in Chrome and Safari on macOS.
299 | */
300 |
301 | [type="search"]::-webkit-search-decoration {
302 | -webkit-appearance: none;
303 | }
304 |
305 | /**
306 | * 1. Correct the inability to style clickable types in iOS and Safari.
307 | * 2. Change font properties to `inherit` in Safari.
308 | */
309 |
310 | ::-webkit-file-upload-button {
311 | -webkit-appearance: button; /* 1 */
312 | font: inherit; /* 2 */
313 | }
314 |
315 | /* Interactive
316 | ========================================================================== */
317 |
318 | /*
319 | * Add the correct display in Edge, IE 10+, and Firefox.
320 | */
321 |
322 | details {
323 | display: block;
324 | }
325 |
326 | /*
327 | * Add the correct display in all browsers.
328 | */
329 |
330 | summary {
331 | display: list-item;
332 | }
333 |
334 | /* Misc
335 | ========================================================================== */
336 |
337 | /**
338 | * Add the correct display in IE 10+.
339 | */
340 |
341 | template {
342 | display: none;
343 | }
344 |
345 | /**
346 | * Add the correct display in IE 10.
347 | */
348 |
349 | [hidden] {
350 | display: none;
351 | }
352 |
--------------------------------------------------------------------------------
/src/styles/_variables.scss:
--------------------------------------------------------------------------------
1 | // SIZES
2 | $smallBreakPoint: 30rem;
3 | $mediumBreakPoint: 48rem;
4 | $largeBreakPoint: 64rem;
5 | $extraLargeBreakPoint: 80rem;
6 |
7 | $horizontalSpace: 0.5rem;
8 | $horizontalSpaceLarge: 1rem;
9 |
10 | // COLOR ROLES
11 | $primary: #ed0000;
12 | $bg-primary: #181818;
13 | $text: #fcfcfc;
14 | $text-secondary: #e0e0e0;
15 | $border: #eee;
16 |
17 | // SHADOWS
18 | $shadowMainBox: 3px 3px 17px 0 rgba($bg-primary, 0.7);
19 |
20 | // TRANSITIONS
21 | $transition-primary: 0.2s ease-in-out;
--------------------------------------------------------------------------------
/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import './_normalize.scss';
2 | @import './_base.scss';
--------------------------------------------------------------------------------
/src/utils/formatTime.js:
--------------------------------------------------------------------------------
1 | export const formatTime = timestamp => {
2 | if (isNaN(timestamp)) return ''
3 | let minutes = Math.floor(timestamp / 60)
4 | let seconds = timestamp - minutes * 60
5 | if (seconds < 10) {
6 | seconds = '0' + seconds
7 | }
8 | timestamp = minutes + ':' + seconds
9 | timestamp = timestamp.split('.')[0]
10 | return timestamp
11 | }
12 |
--------------------------------------------------------------------------------
/src/utils/objectUtils.js:
--------------------------------------------------------------------------------
1 | export const objectHasProps = obj => {
2 | const shouldEval = !!obj && typeof obj === 'object'
3 | if (shouldEval) {
4 | return Object.keys(obj).length > 0
5 | }
6 | return false
7 | }
8 |
9 | export const testCond = (testObj, property, evaluation) => {
10 | return objectHasProps(testObj) ? testObj[property] : evaluation
11 | }
12 |
--------------------------------------------------------------------------------