Hey everyone!
I've been diving into a new project to get my hands dirty with React and to better understand how music players like Spotify, iTunes, and SoundCloud work behind the scenes. This includes things like music libraries and song selection, and it's been a fun learning experience so far.
At the moment, I’m working on an early alpha version of an iTunes-inspired music player. It lets me choose songs from my own music library stored in a Postgres database, and I’ve built playback and music control functionality using a custom component. You can scrub through the track and adjust the volume — just like a real music player!
This is still very much a side project, and I plan to move it off my work Retool account soon. However, I'm hitting a bit of a snag transferring the custom component over to my personal account since it uses React and a model and my personal org doesn't have this component. Once I figure that out, I'll be moving forward with the design and sharing more updates.
For now, here's a sneak peek of what it looks like so far! Hope you all enjoy it, and I’m excited to keep learning from this amazing community!
iFrame code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iTunes-Inspired Music Player</title>
<style>
body, html {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
background-color: #ffffff;
}
.player-container {
display: flex;
align-items: center;
justify-content: space-between;
width: 95%;
height: 40px;
padding: 5px 20px;
background-color: #ffffff; /* iTunes-like background */
border-top: 1px solid #ccc;
position: fixed;
bottom: 0;
left: 0;
right: 0;
}
.left-controls {
display: flex;
align-items: center;
}
.play-pause-button {
background: none;
border: none;
cursor: pointer;
outline: none;
margin-right: 15px;
}
.play-pause-button svg {
fill: #007bff;
width: 24px;
height: 24px;
}
.album-cover {
width: 40px;
height: 40px;
margin-right: 10px;
border-radius: 4px;
background-color: #ddd;
background-size: cover;
background-position: center;
}
.center-info {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
flex: 1;
}
.track-title {
font-size: 14px;
font-weight: bold;
color: #333;
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 250px;
}
.track-artist {
font-size: 10px;
font-weight: bold;
color: #333;
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 250px;
}
.progress-bar-container {
width: 70%;
background-color: #ddd;
border-radius: 4px;
height: 4px;
position: relative;
cursor: pointer;
}
.progress {
height: 100%;
background-color: #007bff;
width: 0;
border-radius: 4px;
transition: width 0.2s ease;
}
.right-controls {
display: flex;
align-items: center;
}
.volume-slider {
width: 80px;
height: 4px;
background: #007bff;
border-radius: 4px;
cursor: pointer;
}
.volume-slider::-webkit-slider-thumb {
width: 10px;
height: 12px;
border-radius: 50%;
background: #fff;
cursor: pointer;
}
</style>
</head>
<body>
<div id="react"></div>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script type="text/babel">
const MusicPlayer = ({ model }) => {
const audioRef = React.useRef(null);
const progressBarRef = React.useRef(null);
const [isPlaying, setIsPlaying] = React.useState(false);
const [progress, setProgress] = React.useState(0);
const [volume, setVolume] = React.useState(1);
const togglePlayPause = () => {
if (isPlaying) {
audioRef.current.pause();
} else {
audioRef.current.play();
}
setIsPlaying(!isPlaying);
};
const updateProgress = () => {
const duration = audioRef.current.duration;
const currentTime = audioRef.current.currentTime;
setProgress((currentTime / duration) * 100);
};
const handleVolumeChange = (e) => {
const newVolume = e.target.value;
setVolume(newVolume);
audioRef.current.volume = newVolume;
};
const seekAudio = (event) => {
const progressBar = progressBarRef.current;
const rect = progressBar.getBoundingClientRect();
const clickX = event.clientX - rect.left;
const width = rect.width;
const clickRatio = clickX / width;
const duration = audioRef.current.duration;
audioRef.current.currentTime = clickRatio * duration;
};
// Extract song title from URL and replace %20 with spaces
const extractTitle = (url) => {
const fileName = decodeURIComponent(url.split('/').pop());
return fileName.replace('.mp3', '').replace(/%20/g, ' ');
};
return (
<div className="player-container">
<div className="left-controls">
<button className="play-pause-button" onClick={togglePlayPause}>
{isPlaying ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
)}
</button>
<div
className="album-cover"
style={{ backgroundImage: `url('${model.art}')` }} /* Use the model for album art */
></div>
</div>
<div className="center-info">
<div className="track-title">{model.title}</div>
<div className="track-artist">{model.artist}</div>
<div
className="progress-bar-container"
ref={progressBarRef}
onClick={seekAudio} /* Add onClick handler here */
>
<div className="progress" style={{ width: `${progress}%` }}></div>
</div>
</div>
<div className="right-controls">
<input
className="volume-slider"
type="range"
min="0"
max="1"
step="0.01"
value={volume}
onChange={handleVolumeChange}
/>
</div>
<audio
ref={audioRef}
src={model.soundURL} /* Use model.soundURL for audio source */
onTimeUpdate={updateProgress}
/>
</div>
);
};
const ConnectedComponent = Retool.connectReactComponent(MusicPlayer);
const container = document.getElementById('react');
const root = ReactDOM.createRoot(container);
root.render(<ConnectedComponent />);
</script>
</body>
</html>
Model Structure for selectedSong
:
{
soundURL: {{ selectedSong.value.songURL }},
artist: {{ selectedSong.value.artist }},
art: {{ selectedSong.value.art }},
title: {{ selectedSong.value.title }}
}
Let me know your thoughts, and feel free to share any tips or ideas!