Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Playlists i Library Management

Spotify to organizacja muzyki - playlists, liked songs, albums w library.

Playlist Structure

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}

Playlist Page

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">
8990        </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                >
150151                </button>
152              </div>
153            </div>
154          ))}
155        </div>
156      </div>
157    </div>
158  );
159}

Create Playlist Modal

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="Moja playlista #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="Dodaj opcjonalny opis"
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}

Podsumowanie 🎓

Playlist structure - tracks, followers, duration ✅ Playlist page - header z 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 z name, description, privacy ✅ Public/private - visibility control ✅ Playlist stats - track count, total duration, followers

W następnym ćwiczeniu dodamy search i recommendations!

Do zobaczenia! 🚀

Przejdź do CodeWorlds