Spotify is all about music organization - playlists, liked songs, albums in library.
1interface Playlist {
2 id: string;
3 name: string;
4 description: string;
5 coverImage: string;
6
7 owner: {
8 id: string;
9 username: string;
10 avatar: string;
11 };
12
13 tracks: Track[];
14 followers: number;
15
16 isPublic: boolean;
17 isCollaborative: boolean; // multiple people can add tracks
18
19 totalDuration: number;
20 createdAt: Date;
21 updatedAt: Date;
22}
23
24interface Library {
25 likedTracks: Track[];
26 savedAlbums: Album[];
27 followedArtists: Artist[];
28 playlists: Playlist[];
29 recentlyPlayed: Track[];
30}1'use client';
2
3import { useState } from 'react';
4
5interface PlaylistPageProps {
6 playlist: Playlist;
7 onPlay: (trackIndex?: number) => void;
8 onLikePlaylist: () => void;
9 onAddTrack: () => void;
10 onRemoveTrack: (trackId: string) => void;
11}
12
13export default function PlaylistPage({
14 playlist,
15 onPlay,
16 onLikePlaylist,
17 onAddTrack,
18 onRemoveTrack
19}: PlaylistPageProps) {
20 const [sortBy, setSortBy] = useState<'added' | 'title' | 'artist' | 'album' | 'duration'>('added');
21
22 const totalDuration = playlist.tracks.reduce((acc, track) => acc + track.duration, 0);
23 const totalHours = Math.floor(totalDuration / 3600);
24 const totalMinutes = Math.floor((totalDuration % 3600) / 60);
25
26 const formatDuration = (seconds: number) => {
27 const mins = Math.floor(seconds / 60);
28 const secs = Math.floor(seconds % 60);
29 return `${mins}:${secs.toString().padStart(2, '0')}`;
30 };
31
32 return (
33 <div className="min-h-screen bg-gradient-to-b from-blue-900 to-black text-white">
34 {/* Header */}
35 <div className="bg-gradient-to-b from-blue-900 to-transparent p-8">
36 <div className="flex items-end gap-6">
37 {/* Cover */}
38 <img
39 src={playlist.coverImage}
40 alt={playlist.name}
41 className="w-60 h-60 rounded shadow-2xl"
42 />
43
44 {/* Info */}
45 <div className="flex-1">
46 <p className="text-sm font-semibold uppercase">Playlist</p>
47 <h1 className="text-7xl font-bold my-4">{playlist.name}</h1>
48 <p className="text-gray-300 mb-4">{playlist.description}</p>
49
50 <div className="flex items-center gap-2 text-sm">
51 <img
52 src={playlist.owner.avatar}
53 alt={playlist.owner.username}
54 className="w-6 h-6 rounded-full"
55 />
56 <span className="font-semibold">{playlist.owner.username}</span>
57 <span>•</span>
58 <span>{playlist.followers.toLocaleString()} followers</span>
59 <span>•</span>
60 <span>{playlist.tracks.length} songs</span>
61 {totalHours > 0 && (
62 <>
63 <span>•</span>
64 <span>{totalHours}h {totalMinutes}min</span>
65 </>
66 )}
67 </div>
68 </div>
69 </div>
70 </div>
71
72 {/* Actions */}
73 <div className="px-8 py-6 flex items-center gap-4">
74 <button
75 onClick={() => onPlay()}
76 className="w-14 h-14 bg-green-500 rounded-full flex items-center justify-center hover:scale-105 transition"
77 >
78 <span className="text-black text-2xl ml-1">▶</span>
79 </button>
80
81 <button
82 onClick={onLikePlaylist}
83 className="text-3xl text-gray-400 hover:text-white"
84 >
85 🤍
86 </button>
87
88 <button className="text-3xl text-gray-400 hover:text-white">
89 ⋯
90 </button>
91 </div>
92
93 {/* Track List */}
94 <div className="px-8 pb-8">
95 {/* Headers */}
96 <div className="grid grid-cols-[16px_6fr_4fr_3fr_minmax(120px,1fr)] gap-4 px-4 py-2 border-b border-gray-700 text-sm text-gray-400 mb-2">
97 <div>#</div>
98 <div>Title</div>
99 <div>Album</div>
100 <div>Date Added</div>
101 <div className="text-right">⏱</div>
102 </div>
103
104 {/* Tracks */}
105 <div>
106 {playlist.tracks.map((track, index) => (
107 <div
108 key={track.id}
109 onDoubleClick={() => onPlay(index)}
110 className="grid grid-cols-[16px_6fr_4fr_3fr_minmax(120px,1fr)] gap-4 px-4 py-2 rounded hover:bg-white hover:bg-opacity-10 group"
111 >
112 <div className="flex items-center text-gray-400">
113 <span className="group-hover:hidden">{index + 1}</span>
114 <button className="hidden group-hover:block">▶</button>
115 </div>
116
117 <div className="flex items-center gap-3">
118 <img
119 src={track.album.coverArt}
120 alt={track.title}
121 className="w-10 h-10 rounded"
122 />
123 <div>
124 <div className="font-semibold">{track.title}</div>
125 <div className="text-sm text-gray-400">
126 {track.artist.name}
127 </div>
128 </div>
129 </div>
130
131 <div className="flex items-center text-sm text-gray-400">
132 {track.album.title}
133 </div>
134
135 <div className="flex items-center text-sm text-gray-400">
136 {new Date().toLocaleDateString()}
137 </div>
138
139 <div className="flex items-center justify-end gap-4">
140 <button className="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-white">
141 {track.isLiked ? '❤️' : '🤍'}
142 </button>
143 <span className="text-sm text-gray-400">
144 {formatDuration(track.duration)}
145 </span>
146 <button
147 onClick={() => onRemoveTrack(track.id)}
148 className="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-white"
149 >
150 ⋯
151 </button>
152 </div>
153 </div>
154 ))}
155 </div>
156 </div>
157 </div>
158 );
159}1'use client';
2
3import { useState } from 'react';
4
5interface CreatePlaylistModalProps {
6 onClose: () => void;
7 onCreate: (name: string, description: string, isPublic: boolean) => void;
8}
9
10export default function CreatePlaylistModal({
11 onClose,
12 onCreate
13}: CreatePlaylistModalProps) {
14 const [name, setName] = useState('');
15 const [description, setDescription] = useState('');
16 const [isPublic, setIsPublic] = useState(true);
17
18 const handleSubmit = (e: React.FormEvent) => {
19 e.preventDefault();
20 if (name.trim()) {
21 onCreate(name, description, isPublic);
22 onClose();
23 }
24 };
25
26 return (
27 <div className="fixed inset-0 bg-black bg-opacity-75 z-50 flex items-center justify-center p-4">
28 <div className="bg-gray-900 rounded-lg max-w-md w-full p-6">
29 <h2 className="text-2xl font-bold text-white mb-6">Create Playlist</h2>
30
31 <form onSubmit={handleSubmit}>
32 <div className="mb-4">
33 <label className="block text-sm font-semibold text-white mb-2">
34 Name
35 </label>
36 <input
37 type="text"
38 value={name}
39 onChange={(e) => setName(e.target.value)}
40 placeholder="My playlist #1"
41 className="w-full bg-gray-800 text-white px-4 py-3 rounded outline-none focus:ring-2 focus:ring-green-500"
42 autoFocus
43 />
44 </div>
45
46 <div className="mb-4">
47 <label className="block text-sm font-semibold text-white mb-2">
48 Description (Optional)
49 </label>
50 <textarea
51 value={description}
52 onChange={(e) => setDescription(e.target.value)}
53 placeholder="Add an optional description"
54 className="w-full bg-gray-800 text-white px-4 py-3 rounded outline-none resize-none focus:ring-2 focus:ring-green-500"
55 rows={3}
56 />
57 </div>
58
59 <div className="mb-6">
60 <label className="flex items-center gap-3 cursor-pointer">
61 <input
62 type="checkbox"
63 checked={isPublic}
64 onChange={(e) => setIsPublic(e.target.checked)}
65 className="w-5 h-5"
66 />
67 <span className="text-white">Make playlist public</span>
68 </label>
69 </div>
70
71 <div className="flex gap-3">
72 <button
73 type="button"
74 onClick={onClose}
75 className="flex-1 py-3 border border-gray-700 rounded-full text-white font-semibold hover:border-white"
76 >
77 Cancel
78 </button>
79 <button
80 type="submit"
81 disabled={!name.trim()}
82 className="flex-1 py-3 bg-green-500 rounded-full text-black font-semibold hover:bg-green-400 disabled:bg-gray-700 disabled:text-gray-500"
83 >
84 Create
85 </button>
86 </div>
87 </form>
88 </div>
89 </div>
90 );
91}✅ Playlist structure - tracks, followers, duration ✅ Playlist page - header with cover art, track list ✅ Track grid - #, title, album, date, duration ✅ Hover actions - play, like, remove track ✅ Double-click to play - quick playback ✅ Create playlist - modal with name, description, privacy ✅ Public/private - visibility control ✅ Playlist stats - track count, total duration, followers
In the next exercise we'll add search and recommendations!
See you soon! 🚀