Cours Vibe coding avec l'IA · Module 11: Clone de Spotify

Playlists et gestion de la bibliothèque

4 min de lecture
Dans cette leçon4

Spotify, c'est avant tout l'organisation de la musique - playlists, titres likés, albums dans la bibliothèque.

Structure d'une playlist

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; // plusieurs personnes peuvent ajouter des morceaux
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}

Page de la playlist

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      {/* En-tête */}
35      <div className="bg-gradient-to-b from-blue-900 to-transparent p-8">
36        <div className="flex items-end gap-6">
37          {/* Pochette */}
38          <img
39            src={playlist.coverImage}
40            alt={playlist.name}
41            className="w-60 h-60 rounded shadow-2xl"
42          />
43
44          {/* Infos */}
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()} abonnés</span>
59              <span>•</span>
60              <span>{playlist.tracks.length} titres</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-sm font-semibold text-gray-400 hover:text-white"
84        >
85          J’aime
86        </button>
87
88        <button className="text-3xl text-gray-400 hover:text-white">
89          ⋯
90        </button>
91      </div>
92
93      {/* Liste des morceaux */}
94      <div className="px-8 pb-8">
95        {/* En-têtes */}
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>Titre</div>
99          <div>Album</div>
100          <div>Date d'ajout</div>
101          <div className="text-right">⏱</div>
102        </div>
103
104        {/* Morceaux */}
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-white/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 ? 'Aimé' : 'J’aime'}
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}

Fenêtre de création de playlist

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/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">Créer une 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              Nom
35            </label>
36            <input
37              type="text"
38              value={name}
39              onChange={(e) => setName(e.target.value)}
40              placeholder="Ma playlist n°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 (facultatif)
49            </label>
50            <textarea
51              value={description}
52              onChange={(e) => setDescription(e.target.value)}
53              placeholder="Ajoute une description facultative"
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">Rendre la playlist publique</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              Annuler
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              Créer
85            </button>
86          </div>
87        </form>
88      </div>
89    </div>
90  );
91}

Récapitulatif

Structure d'une playlist - morceaux, abonnés, durée Page de la playlist - en-tête avec pochette, liste des morceaux Grille des morceaux - #, titre, album, date, durée Actions au survol - lire, liker, retirer un morceau Double-clic pour lire - lecture rapide Créer une playlist - fenêtre avec nom, description, confidentialité Public/privé - contrôle de la visibilité Statistiques de la playlist - nombre de morceaux, durée totale, abonnés

Dans le prochain exercice, nous ajouterons la recherche et les recommandations !

À très bientôt !

Code de cette leçon : App.tsx
1'use client';
2
3import { useState } from 'react';
4
5interface Track {
6  id: string;
7  title: string;
8  artist: string;
9  coverArt: string;
10  duration: number;
11}
12
13const mockQueue: Track[] = [
14  {
15    id: '1',
16    title: "The Pirate's Anthem",
17    artist: 'Captain Redbeard',
18    coverArt: 'https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=200&h=200&fit=crop',
19    duration: 240
20  },
21  {
22    id: '2',
23    title: 'Sailing the Seven Seas',
24    artist: 'Crew Council',
25    coverArt: 'https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=200&h=200&fit=crop',
26    duration: 195
27  },
28  {
29    id: '3',
30    title: 'Treasure Hunt',
31    artist: 'Blackbeard Band',
32    coverArt: 'https://images.unsplash.com/photo-1514320291840-2e0a9bf2a9ae?w=200&h=200&fit=crop',
33    duration: 210
34  }
35];
36
37export default function Queue() {
38  const [queue, setQueue] = useState(mockQueue);
39  const [currentIndex, setCurrentIndex] = useState(0);
40
41  const formatDuration = (seconds: number) => {
42    const mins = Math.floor(seconds / 60);
43    const secs = Math.floor(seconds % 60);
44    return `${mins}:${secs.toString().padStart(2, '0')}`;
45  };
46
47  const handleRemove = (index: number) => {
48    setQueue(queue.filter((_, i) => i !== index));
49  };
50
51  return (
52    <div className="min-h-screen bg-black text-white flex">
53      <div className="flex-1 bg-gray-950"></div>
54
55      <div className="w-80 bg-gray-900 border-l border-gray-800 flex flex-col">
56        {/* En-tête */}
57        <div className="p-4 border-b border-gray-800">
58          <div className="flex items-center justify-between mb-2">
59            <h3 className="font-bold">File d'attente</h3>
60            <button
61              onClick={() => setQueue([])}
62              className="text-xs text-gray-400 hover:text-white"
63            >
64              Vider
65            </button>
66          </div>
67          <p className="text-xs text-gray-400">{queue.length} morceaux</p>
68        </div>
69
70        {/* En cours de lecture */}
71        {queue[currentIndex] && (
72          <div className="p-4 bg-gray-800 border-b border-gray-700">
73            <p className="text-xs text-gray-400 mb-2">En cours de lecture</p>
74            <div className="flex items-center gap-3">
75              <img
76                src={queue[currentIndex].coverArt}
77                alt={queue[currentIndex].title}
78                className="w-12 h-12 rounded"
79              />
80              <div className="flex-1 min-w-0">
81                <div className="font-semibold text-sm truncate">
82                  {queue[currentIndex].title}
83                </div>
84                <div className="text-xs text-gray-400 truncate">
85                  {queue[currentIndex].artist}
86                </div>
87              </div>
88            </div>
89          </div>
90        )}
91
92        {/* À suivre */}
93        <div className="flex-1 overflow-y-auto p-4">
94          <p className="text-xs text-gray-400 mb-3">À suivre</p>
95          <div className="space-y-2">
96            {queue.slice(currentIndex + 1).map((track, idx) => {
97              const actualIndex = currentIndex + 1 + idx;
98              return (
99                <div
100                  key={track.id}
101                  onClick={() => setCurrentIndex(actualIndex)}
102                  className="flex items-center gap-3 p-2 hover:bg-gray-800 rounded cursor-pointer group"
103                >
104                  <img
105                    src={track.coverArt}
106                    alt={track.title}
107                    className="w-10 h-10 rounded"
108                  />
109                  <div className="flex-1 min-w-0">
110                    <div className="text-sm truncate">{track.title}</div>
111                    <div className="text-xs text-gray-400 truncate">
112                      {track.artist}
113                    </div>
114                  </div>
115                  <span className="text-xs text-gray-400">
116                    {formatDuration(track.duration)}
117                  </span>
118                  <button
119                    onClick={(e) => {
120                      e.stopPropagation();
121                      handleRemove(actualIndex);
122                    }}
123                    className="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-white"
124                  >
125                    ×
126                  </button>
127                </div>
128              );
129            })}
130          </div>
131        </div>
132      </div>
133    </div>
134  );
135}

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Que contient une Playlist ?

  2. 2. Que permet le champ isCollaborative dans Playlist ?

Voici 2 des 7 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Clique dans l'ordre

    Clique pour construire l'appel de création de playlist :

  • Clique dans l'ordre

    Clique pour construire l'état du nom de la playlist :

  • Tri horizontal

    Ordonne les colonnes de la liste des morceaux d'une playlist :

  • Tri horizontal

    Ordonne les éléments d'une ligne de morceau dans la liste, de gauche à droite :

  • Éditeur de code

    Composant d'affichage de playlist

  • Clique dans l'ordre

    Clique pour construire la somme de la durée des morceaux :

  • Tri vertical

    Ordonne les champs de la structure Library tels qu'ils apparaissent dans le code :

  • Tri vertical

    Ordonne les sections de PlaylistPage de haut en bas :

  • Tri horizontal

    Ordonne les éléments pour lire le nom du propriétaire de la playlist sur une seule ligne :

  • Tri vertical

    Ordonne les étapes de création d'une playlist dans la modale :

  • Éditeur de code

    Interface de création de playlist

Articles utiles