Welcome @name! Time to create Sea Shanty Sounds - a pirate music streaming platform! 🎵
This will be like Spotify - music player, playlists, albums, artists, and personalized recommendations.
YouTube:
TikTok:
Spotify:
1interface Track {
2 id: string;
3 title: string;
4 artist: Artist;
5 album: Album;
6
7 // Audio
8 audioUrl: string;
9 duration: number; // seconds
10 previewUrl?: string; // 30s preview
11
12 // Metadata
13 genre: string[];
14 releaseDate: Date;
15 explicit: boolean;
16
17 // Engagement
18 plays: number;
19 likes: number;
20 inPlaylists: number;
21
22 // Lyrics
23 hasLyrics: boolean;
24 lyrics?: Lyric[];
25
26 // User state
27 isLiked: boolean;
28 isInLibrary: boolean;
29}
30
31interface Artist {
32 id: string;
33 name: string;
34 avatar: string;
35 banner?: string;
36 verified: boolean;
37 followers: number;
38 monthlyListeners: number;
39}
40
41interface Album {
42 id: string;
43 title: string;
44 artist: Artist;
45 coverArt: string;
46 releaseDate: Date;
47 tracks: Track[];
48 totalDuration: number;
49 type: 'album' | 'single' | 'ep';
50}
51
52interface Lyric {
53 timestamp: number; // seconds
54 text: string;
55}1'use client';
2
3import { useState, useRef, useEffect } from 'react';
4
5interface MusicPlayerProps {
6 currentTrack: Track | null;
7 queue: Track[];
8 onNext: () => void;
9 onPrevious: () => void;
10 onShuffle: () => void;
11 onRepeat: () => void;
12}
13
14export default function MusicPlayer({
15 currentTrack,
16 queue,
17 onNext,
18 onPrevious,
19 onShuffle,
20 onRepeat
21}: MusicPlayerProps) {
22 const [isPlaying, setIsPlaying] = useState(false);
23 const [currentTime, setCurrentTime] = useState(0);
24 const [volume, setVolume] = useState(100);
25 const [isMuted, setIsMuted] = useState(false);
26 const [shuffleOn, setShuffleOn] = useState(false);
27 const [repeatMode, setRepeatMode] = useState<'off' | 'all' | 'one'>('off');
28
29 const audioRef = useRef<HTMLAudioElement>(null);
30
31 useEffect(() => {
32 if (audioRef.current && currentTrack) {
33 audioRef.current.src = currentTrack.audioUrl;
34 if (isPlaying) {
35 audioRef.current.play();
36 }
37 }
38 }, [currentTrack]);
39
40 useEffect(() => {
41 const audio = audioRef.current;
42 if (!audio) return;
43
44 const updateTime = () => setCurrentTime(audio.currentTime);
45 const handleEnded = () => {
46 if (repeatMode === 'one') {
47 audio.currentTime = 0;
48 audio.play();
49 } else {
50 onNext();
51 }
52 };
53
54 audio.addEventListener('timeupdate', updateTime);
55 audio.addEventListener('ended', handleEnded);
56
57 return () => {
58 audio.removeEventListener('timeupdate', updateTime);
59 audio.removeEventListener('ended', handleEnded);
60 };
61 }, [repeatMode, onNext]);
62
63 const togglePlay = () => {
64 if (audioRef.current) {
65 if (isPlaying) {
66 audioRef.current.pause();
67 } else {
68 audioRef.current.play();
69 }
70 setIsPlaying(!isPlaying);
71 }
72 };
73
74 const handleSeek = (time: number) => {
75 if (audioRef.current) {
76 audioRef.current.currentTime = time;
77 setCurrentTime(time);
78 }
79 };
80
81 const handleVolumeChange = (newVolume: number) => {
82 if (audioRef.current) {
83 audioRef.current.volume = newVolume / 100;
84 setVolume(newVolume);
85 setIsMuted(newVolume === 0);
86 }
87 };
88
89 const toggleMute = () => {
90 if (audioRef.current) {
91 if (isMuted) {
92 audioRef.current.volume = volume / 100;
93 setIsMuted(false);
94 } else {
95 audioRef.current.volume = 0;
96 setIsMuted(true);
97 }
98 }
99 };
100
101 const toggleShuffle = () => {
102 setShuffleOn(!shuffleOn);
103 onShuffle();
104 };
105
106 const cycleRepeat = () => {
107 const modes: Array<'off' | 'all' | 'one'> = ['off', 'all', 'one'];
108 const currentIndex = modes.indexOf(repeatMode);
109 const nextMode = modes[(currentIndex + 1) % modes.length];
110 setRepeatMode(nextMode);
111 onRepeat();
112 };
113
114 const formatTime = (seconds: number) => {
115 const mins = Math.floor(seconds / 60);
116 const secs = Math.floor(seconds % 60);
117 return `${mins}:${secs.toString().padStart(2, '0')}`;
118 };
119
120 if (!currentTrack) {
121 return (
122 <div className="h-24 bg-gray-900 border-t border-gray-800 flex items-center justify-center text-gray-500">
123 No track playing
124 </div>
125 );
126 }
127
128 return (
129 <>
130 <audio ref={audioRef} />
131
132 <div className="h-24 bg-gray-900 border-t border-gray-800 px-4 flex items-center gap-4">
133 {/* Track Info */}
134 <div className="flex items-center gap-3 w-1/4">
135 <img
136 src={currentTrack.album.coverArt}
137 alt={currentTrack.title}
138 className="w-14 h-14 rounded"
139 />
140 <div className="flex-1 min-w-0">
141 <div className="font-semibold text-white text-sm truncate">
142 {currentTrack.title}
143 </div>
144 <div className="text-xs text-gray-400 truncate">
145 {currentTrack.artist.name}
146 </div>
147 </div>
148 <button className="text-gray-400 hover:text-white">
149 {currentTrack.isLiked ? '❤️' : '🤍'}
150 </button>
151 </div>
152
153 {/* Playback Controls */}
154 <div className="flex-1 flex flex-col items-center gap-2">
155 {/* Buttons */}
156 <div className="flex items-center gap-4">
157 <button
158 onClick={toggleShuffle}
159 className={`text-gray-400 hover:text-white ${shuffleOn ? 'text-green-500' : ''}`}
160 >
161 🔀
162 </button>
163
164 <button
165 onClick={onPrevious}
166 className="text-gray-400 hover:text-white"
167 >
168 ⏮
169 </button>
170
171 <button
172 onClick={togglePlay}
173 className="w-8 h-8 bg-white rounded-full flex items-center justify-center hover:scale-105 transition"
174 >
175 <span className="text-black text-xl">
176 {isPlaying ? '⏸' : '▶'}
177 </span>
178 </button>
179
180 <button
181 onClick={onNext}
182 className="text-gray-400 hover:text-white"
183 >
184 ⏭
185 </button>
186
187 <button
188 onClick={cycleRepeat}
189 className={`text-gray-400 hover:text-white ${
190 repeatMode !== 'off' ? 'text-green-500' : ''
191 }`}
192 >
193 {repeatMode === 'one' ? '🔂' : '🔁'}
194 </button>
195 </div>
196
197 {/* Progress Bar */}
198 <div className="w-full flex items-center gap-2">
199 <span className="text-xs text-gray-400 w-10 text-right">
200 {formatTime(currentTime)}
201 </span>
202 <div className="flex-1 group">
203 <input
204 type="range"
205 min={0}
206 max={currentTrack.duration}
207 value={currentTime}
208 onChange={(e) => handleSeek(parseFloat(e.target.value))}
209 className="w-full h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer group-hover:h-1.5 transition-all"
210 />
211 </div>
212 <span className="text-xs text-gray-400 w-10">
213 {formatTime(currentTrack.duration)}
214 </span>
215 </div>
216 </div>
217
218 {/* Volume & Queue */}
219 <div className="flex items-center gap-3 w-1/4 justify-end">
220 <button className="text-gray-400 hover:text-white">
221 📋
222 </button>
223
224 <button
225 onClick={toggleMute}
226 className="text-gray-400 hover:text-white"
227 >
228 {isMuted || volume === 0 ? '🔇' : volume < 50 ? '🔉' : '🔊'}
229 </button>
230
231 <input
232 type="range"
233 min={0}
234 max={100}
235 value={isMuted ? 0 : volume}
236 onChange={(e) => handleVolumeChange(parseInt(e.target.value))}
237 className="w-24 h-1 bg-gray-600 rounded-lg appearance-none cursor-pointer"
238 />
239
240 <button className="text-gray-400 hover:text-white">
241 ⛶
242 </button>
243 </div>
244 </div>
245 </>
246 );
247}1'use client';
2
3interface QueueProps {
4 queue: Track[];
5 currentIndex: number;
6 onTrackClick: (index: number) => void;
7 onRemove: (index: number) => void;
8 onClear: () => void;
9}
10
11export default function Queue({
12 queue,
13 currentIndex,
14 onTrackClick,
15 onRemove,
16 onClear
17}: QueueProps) {
18 const formatDuration = (seconds: number) => {
19 const mins = Math.floor(seconds / 60);
20 const secs = Math.floor(seconds % 60);
21 return `${mins}:${secs.toString().padStart(2, '0')}`;
22 };
23
24 return (
25 <div className="w-80 bg-gray-900 border-l border-gray-800 flex flex-col h-screen">
26 {/* Header */}
27 <div className="p-4 border-b border-gray-800">
28 <div className="flex items-center justify-between mb-2">
29 <h3 className="font-bold text-white">Queue</h3>
30 <button
31 onClick={onClear}
32 className="text-xs text-gray-400 hover:text-white"
33 >
34 Clear
35 </button>
36 </div>
37 <p className="text-xs text-gray-400">
38 {queue.length} tracks
39 </p>
40 </div>
41
42 {/* Now Playing */}
43 {queue[currentIndex] && (
44 <div className="p-4 bg-gray-800 border-b border-gray-700">
45 <p className="text-xs text-gray-400 mb-2">Now Playing</p>
46 <div className="flex items-center gap-3">
47 <img
48 src={queue[currentIndex].album.coverArt}
49 alt={queue[currentIndex].title}
50 className="w-12 h-12 rounded"
51 />
52 <div className="flex-1 min-w-0">
53 <div className="font-semibold text-white text-sm truncate">
54 {queue[currentIndex].title}
55 </div>
56 <div className="text-xs text-gray-400 truncate">
57 {queue[currentIndex].artist.name}
58 </div>
59 </div>
60 </div>
61 </div>
62 )}
63
64 {/* Next Up */}
65 <div className="flex-1 overflow-y-auto">
66 <div className="p-4">
67 <p className="text-xs text-gray-400 mb-3">Next Up</p>
68 <div className="space-y-2">
69 {queue.slice(currentIndex + 1).map((track, idx) => {
70 const actualIndex = currentIndex + 1 + idx;
71 return (
72 <div
73 key={track.id}
74 onClick={() => onTrackClick(actualIndex)}
75 className="flex items-center gap-3 p-2 hover:bg-gray-800 rounded cursor-pointer group"
76 >
77 <img
78 src={track.album.coverArt}
79 alt={track.title}
80 className="w-10 h-10 rounded"
81 />
82 <div className="flex-1 min-w-0">
83 <div className="text-sm text-white truncate">
84 {track.title}
85 </div>
86 <div className="text-xs text-gray-400 truncate">
87 {track.artist.name}
88 </div>
89 </div>
90 <span className="text-xs text-gray-400">
91 {formatDuration(track.duration)}
92 </span>
93 <button
94 onClick={(e) => {
95 e.stopPropagation();
96 onRemove(actualIndex);
97 }}
98 className="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-white"
99 >
100 ✕
101 </button>
102 </div>
103 );
104 })}
105 </div>
106 </div>
107 </div>
108 </div>
109 );
110}✅ Music player - play/pause, next/previous, seek ✅ Playback controls - shuffle, repeat (off/all/one) ✅ Progress bar - seek to any position ✅ Volume control - slider + mute button ✅ Track info - title, artist, album art ✅ Queue management - view queue, skip to track, remove ✅ Time formatting - MM:SS display ✅ Auto-advance - next track when current ends
In the next exercise we'll add playlists and library!
See you soon! 🚀