Hotlink Beats
I decided to buy the Creative React and Redux course by Ed, a developer and YouTuber.
The first project was a music app which “hot links” songs from chillhop.com, a music site which creates free music for content creators to add music to their videos or streams.
Getting the data⌗
The song library gets imported into the app using a JavaScript file, data.js
, which exports a function, ChillHop()
, which returns an array of objects. Each object in the array represents 1 song.
Playing the music⌗
The main App imports a Nav, a Song, a Player, and a Library. The main App also renders the HTML <audio>
tag. The useRef
React hook to add a ref
attribute, audioRef
, so that we can reference the <audio>
node in the DOM… I think.
useRef is like a “box” that can hold a mutable value in its .current property
Source: reactjs.org
<audio
onEnded={songEndHandler}
onLoadedMetadata={timeUpdateHandler}
onTimeUpdate={timeUpdateHandler}
ref={audioRef}
src={currentSong.audio}
volume={songInfo.volume}
></audio>
The audioRef
state gets passed as a prop to the Player
and the Library
components: both of these components can change which song is currently playing, and when the current song changes or gets paused, audioRef.current.pause()
or audioRef.current.play()
needs to fire.
What I added on beyond the tutorial⌗
The tutorial did not include a volume control on the song. Every music player needs volume control! So I set about creating it.
The new feature did not require any new components: the UI was done in Player.jsx
. It also did not create new state; instead, I added volume: 0.5
to the existing songInfo
state.
The next few steps included creating the markup, adding styling, and adding a JavaScript event handler to the onChange
event on the volume input slider.
Volume control display⌗
The first item in the container div is an <input>
with type range
. It uses the songInfo.volume
state to know whats its value
should be. The min
and max
properties restrict the input from 0
to 1
because these are the expected values for <audio>
. The step
was set to 0.1
because at 1
, the dragging animation did not feel very nice.
<div className="volume-control">
<input
type="range"
min={0}
max={1}
step={0.1}
onChange={volumeDragHandler}
value={songInfo.volume}
/>
<div
className="animate-volume"
style={transformVolumeRange}
></div>
</div>
There is an illusory second div, animate-volume
, which is used to hide or show the input. Its style
property is set to a JavaScript object, which uses CSS
to move the div
in the x-plane. Because translateX
uses percentage, and <audio volume>
uses decimals (0.0 to 1.0), the songInfo.volume
state needs to be multiplied by 100.
const transformVolumeRange = {
transform: `translateX(${songInfo.volume * 100}%)`
};
The animate-volume
div gets placed absolutely within the main div.
.volume-control {
background-color: $track_color;
border-radius: $volume_height;
height: $volume_height;
overflow: hidden;
position: relative;
margin-top: $volume_height;
width: 20%;
input[type="range"]::-webkit-slider-thumb {
height: $volume_height;
width: $volume_height;
}
.animate-volume {
background: $shadow_color;
height: 100%;
left: 0%;
position: absolute;
pointer-events: none;
top: 0%;
width: 100%;
}
}
Volume Drag Event Handler⌗
When the volume input
gets dragged to adjust the volume, it fires the volumeDragHandler
. This event handler gets the input volume, updates the volume on the <audio>
element, and changes the songInfo
state, preserving all other values, but setting the volume
equal to the volume received from the input.
const volumeDragHandler = (event) => {
let volume = event.target.value;
audioRef.current.volume = volume;
setSongInfo({...songInfo, volume: volume});
}
Favorite Snippet of Code⌗
Here is my favorite piece of code from this project: Death by ternary operator because the ternary operator is great in my opinion.
This is the event handler for when either the fast-forward or rewind buttons get clicked, deciding which song in the array of song objects should be played next.
const skipHandler = async (direction) => {
const curIdx = songs.indexOf(currentSong);
let newIdx = 0;
direction === 'forward' // you want to skip forward?
? curIdx === songs.length - 1 // true: you're on the last song?
? newIdx = 0 // true: go to first song
: newIdx = curIdx + 1 // false: go to next song
: curIdx === 0 // not forward. you're on the first song?
? newIdx = songs.length - 1 // true: go to last song
: newIdx = curIdx - 1 // false: go to previous song
await setCurrentSong(songs[newIdx]); // pause JS run-time until setCurrentSong is done
if (isPlaying) audioRef.current.play(); // play song if applicable
}