Cours Vibe coding avec l'IA · Module 10: Clone de TikTok

Fil de vidéos et page Pour toi

6 min de lecture
Dans cette leçon5

Bienvenue ! Il est temps de créer Treasure Reels - une plateforme de courtes vidéos de pirates !

Ce sera comme TikTok - vidéos verticales courtes, algorithme Pour toi, sons, effets et fonctionnalités duo/collage.

TikTok vs YouTube/Instagram

YouTube :

  • Vidéo horizontale au format long (10+ min)
  • Abonnements et recommandations
  • Commentaires sous les vidéos

Instagram Reels :

  • Vidéo verticale courte dans l'onglet Stories/Reels
  • Surtout des photos + la vidéo en complément
  • Disposition en grille

TikTok :

  • Pure vidéo verticale courte (15-60 s)
  • Algorithme de la page Pour toi (très personnalisé)
  • Bibliothèque de sons et tendances audio
  • Duo, Collage, Effets
  • Fil immersif en plein écran

Structure de la vidéo

1interface Video {
2  id: string;
3  authorId: string;
4  author: {
5    id: string;
6    username: string;
7    avatar: string;
8    isVerified: boolean;
9  };
10
11  // Contenu de la vidéo
12  videoUrl: string;
13  thumbnail: string;
14  duration: number; // secondes
15
16  // Légende
17  caption: string;
18  hashtags: string[]; // #treasurehunt, #piratelife
19
20  // Audio
21  sound: Sound;
22
23  // Engagement
24  likes: number;
25  comments: number;
26  shares: number;
27  views: number;
28
29  // Engagement de l'utilisateur
30  hasLiked: boolean;
31  hasBookmarked: boolean;
32  isFollowingAuthor: boolean;
33
34  createdAt: Date;
35}
36
37interface Sound {
38  id: string;
39  name: string; // "Pirate's Anthem - Redbeard"
40  author: string;
41  duration: number;
42  useCount: number; // combien de vidéos utilisent ce son
43  thumbnailUrl?: string;
44}

Fil Pour toi

1'use client';
2
3import { useState, useRef, useEffect } from 'react';
4
5interface FeedProps {
6  videos: Video[];
7  onLike: (videoId: string) => void;
8  onFollow: (authorId: string) => void;
9  onComment: (videoId: string) => void;
10  onShare: (videoId: string) => void;
11}
12
13export default function ForYouFeed({
14  videos,
15  onLike,
16  onFollow,
17  onComment,
18  onShare
19}: FeedProps) {
20  const [currentIndex, setCurrentIndex] = useState(0);
21  const [isPlaying, setIsPlaying] = useState(true);
22  const videoRefs = useRef<(HTMLVideoElement | null)[]>([]);
23
24  const currentVideo = videos[currentIndex];
25
26  // Lecture automatique de la vidéo courante
27  useEffect(() => {
28    const video = videoRefs.current[currentIndex];
29    if (video) {
30      if (isPlaying) {
31        video.play();
32      } else {
33        video.pause();
34      }
35    }
36  }, [currentIndex, isPlaying]);
37
38  const handleScroll = (direction: 'up' | 'down') => {
39    if (direction === 'down' && currentIndex < videos.length - 1) {
40      setCurrentIndex(currentIndex + 1);
41    } else if (direction === 'up' && currentIndex > 0) {
42      setCurrentIndex(currentIndex - 1);
43    }
44  };
45
46  const handleKeyDown = (e: React.KeyboardEvent) => {
47    if (e.key === 'ArrowUp') handleScroll('up');
48    if (e.key === 'ArrowDown') handleScroll('down');
49    if (e.key === ' ') {
50      e.preventDefault();
51      setIsPlaying(!isPlaying);
52    }
53  };
54
55  return (
56    <div
57      className="h-screen overflow-hidden bg-black relative"
58      onKeyDown={handleKeyDown}
59      tabIndex={0}
60    >
61      {/* Conteneur de la vidéo */}
62      <div
63        className="absolute inset-0 transition-transform duration-300"
64        style={{
65          transform: `translateY(-${currentIndex * 100}%)`
66        }}
67      >
68        {videos.map((video, index) => (
69          <div
70            key={video.id}
71            className="h-screen w-full relative flex items-center justify-center"
72          >
73            {/* Vidéo */}
74            <video
75              ref={el => videoRefs.current[index] = el}
76              src={video.videoUrl}
77              className="h-full w-auto max-w-full object-contain"
78              loop
79              playsInline
80              onClick={() => setIsPlaying(!isPlaying)}
81            />
82
83            {/* Indicateur Lecture/Pause */}
84            {!isPlaying && index === currentIndex && (
85              <div className="absolute inset-0 flex items-center justify-center">
86                <div className="w-20 h-20 bg-black/50 rounded-full flex items-center justify-center">
87                  <span className="text-white text-4xl">▶</span>
88                </div>
89              </div>
90            )}
91
92            {/* Superposition d'infos de la vidéo */}
93            {index === currentIndex && (
94              <VideoInfoOverlay
95                video={video}
96                onLike={() => onLike(video.id)}
97                onFollow={() => onFollow(video.author.id)}
98                onComment={() => onComment(video.id)}
99                onShare={() => onShare(video.id)}
100              />
101            )}
102          </div>
103        ))}
104      </div>
105
106      {/* Flèches de navigation */}
107      <div className="absolute right-4 top-1/2 transform -translate-y-1/2 flex flex-col gap-4">
108        <button
109          onClick={() => handleScroll('up')}
110          disabled={currentIndex === 0}
111          className="w-12 h-12 bg-white/20 hover:bg-white/30 disabled:bg-white/10 rounded-full flex items-center justify-center text-white"
112        >
113          ▲
114        </button>
115        <button
116          onClick={() => handleScroll('down')}
117          disabled={currentIndex === videos.length - 1}
118          className="w-12 h-12 bg-white/20 hover:bg-white/30 disabled:bg-white/10 rounded-full flex items-center justify-center text-white"
119        >
120          ▼
121        </button>
122      </div>
123
124      {/* Indicateur de progression */}
125      <div className="absolute top-4 right-4 text-white text-sm bg-black/50 px-3 py-1 rounded-full">
126        {currentIndex + 1} / {videos.length}
127      </div>
128    </div>
129  );
130}

Superposition d'infos de la vidéo

1interface VideoInfoOverlayProps {
2  video: Video;
3  onLike: () => void;
4  onFollow: () => void;
5  onComment: () => void;
6  onShare: () => void;
7}
8
9function VideoInfoOverlay({
10  video,
11  onLike,
12  onFollow,
13  onComment,
14  onShare
15}: VideoInfoOverlayProps) {
16  return (
17    <>
18      {/* Actions du côté droit */}
19      <div className="absolute right-4 bottom-20 flex flex-col gap-6 items-center">
20        {/* Avatar de l'auteur */}
21        <div className="relative">
22          <img
23            src={video.author.avatar}
24            alt={video.author.username}
25            className="w-12 h-12 rounded-full border-2 border-white"
26          />
27          {!video.isFollowingAuthor && (
28            <button
29              onClick={onFollow}
30              className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 w-6 h-6 bg-red-500 rounded-full text-white text-xl flex items-center justify-center"
31            >
32              +
33            </button>
34          )}
35          {video.author.isVerified && (
36            <div className="absolute -top-1 -right-1 w-4 h-4 bg-blue-500 rounded-full flex items-center justify-center text-white text-xs">
37              √
38            </div>
39          )}
40        </div>
41
42        {/* J’aime */}
43        <button onClick={onLike} className="flex flex-col items-center gap-1">
44          <div
45            className={`text-sm font-semibold transition ${
46              video.hasLiked ? 'text-red-500' : 'text-white'
47            }`}
48          >
49            {video.hasLiked ? 'Aimé' : 'J’aime'}
50          </div>
51          <span className="text-white text-xs font-semibold">
52            {formatCount(video.likes)}
53          </span>
54        </button>
55
56        {/* Commentaire */}
57        <button onClick={onComment} className="flex flex-col items-center gap-1">
58          <div className="text-sm font-semibold text-white">Commenter</div>
59          <span className="text-white text-xs font-semibold">
60            {formatCount(video.comments)}
61          </span>
62        </button>
63
64        {/* Favori */}
65        <button className="flex flex-col items-center gap-1">
66          <div
67            className={`text-sm font-semibold transition ${
68              video.hasBookmarked ? 'text-yellow-400' : 'text-white'
69            }`}
70          >
71            {video.hasBookmarked ? 'Enregistré' : 'Enregistrer'}
72          </div>
73        </button>
74
75        {/* Partager */}
76        <button onClick={onShare} className="flex flex-col items-center gap-1">
77          <div className="text-sm font-semibold text-white">Partager</div>
78          <span className="text-white text-xs font-semibold">
79            {formatCount(video.shares)}
80          </span>
81        </button>
82
83        {/* Son */}
84        <button className="w-10 h-10 rounded-full overflow-hidden border-2 border-white animate-spin-slow">
85          <img
86            src={video.sound.thumbnailUrl || video.author.avatar}
87            alt={video.sound.name}
88            className="w-full h-full object-cover"
89          />
90        </button>
91      </div>
92
93      {/* Infos du bas */}
94      <div className="absolute bottom-4 left-4 right-20">
95        {/* Auteur */}
96        <div className="flex items-center gap-2 mb-2">
97          <span className="text-white font-bold">@{video.author.username}</span>
98          {video.author.isVerified && (
99            <span className="text-blue-400">√</span>
100          )}
101        </div>
102
103        {/* Légende */}
104        <p className="text-white text-sm mb-2 line-clamp-2">
105          {video.caption}
106        </p>
107
108        {/* Hashtags */}
109        {video.hashtags.length > 0 && (
110          <div className="flex flex-wrap gap-2 mb-3">
111            {video.hashtags.map((tag, idx) => (
112              <span key={idx} className="text-white text-sm font-semibold">
113                #{tag}
114              </span>
115            ))}
116          </div>
117        )}
118
119        {/* Son */}
120        <div className="flex items-center gap-2 bg-black/40 px-3 py-1.5 rounded-full max-w-xs">
121          <span className="text-white text-xs">Musique</span>
122          <span className="text-white text-xs truncate">
123            {video.sound.name}
124          </span>
125        </div>
126      </div>
127    </>
128  );
129}
130
131function formatCount(count: number): string {
132  if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
133  if (count >= 1000) return `${(count / 1000).toFixed(1)}K`;
134  return count.toString();
135}

Résumé

Fil de vidéos verticales - défilement immersif en plein écran Algorithme Pour toi - contenu personnalisé Superposition sur la vidéo - auteur, légende, hashtags, son Actions du côté droit - j'aime, commentaire, favori, partage Navigation par défilement - balayage haut/bas entre les vidéos Lecture automatique - la vidéo se lit automatiquement quand elle est visible Lecture/pause - appuie sur la vidéo pour basculer Indicateur de son - disque tournant avec les infos audio Bouton Suivre - abonnement rapide depuis l'avatar

Dans le prochain exercice, nous ajouterons la bibliothèque de sons et la création de vidéos !

À bientôt !

Code de cette leçon : App.jsx
1// Fil de vidéos verticales
2import { useState } from 'react';
3
4const videos = [
5  {
6    id: 1,
7    author: { username: 'CaptainJack', avatar: '', isVerified: true },
8    caption: 'J\'ai trouvé le trésor ! #TreasureHunt #PirateLife',
9    hashtags: ['TreasureHunt', 'PirateLife'],
10    likes: 125000,
11    comments: 3400,
12    shares: 890,
13    sound: { name: 'Pirate\'s Anthem - Redbeard' },
14    hasLiked: false,
15    thumbnail: ''
16  },
17  {
18    id: 2,
19    author: { username: 'BlackbeardCrew', avatar: '', isVerified: false },
20    caption: 'Bataille épique en mer ! #SeaBattle #Pirates',
21    hashtags: ['SeaBattle', 'Pirates'],
22    likes: 89000,
23    comments: 2100,
24    shares: 560,
25    sound: { name: 'Battle Theme - Morgan' },
26    hasLiked: false,
27    thumbnail: ''
28  },
29  {
30    id: 3,
31    author: { username: 'TreasureHunter', avatar: '', isVerified: true },
32    caption: 'Carte vers l\'Isla de Muerta #Map #SecretIsland',
33    hashtags: ['Map', 'SecretIsland'],
34    likes: 234000,
35    comments: 5600,
36    shares: 1200,
37    sound: { name: 'Adventure Awaits - Elizabeth' },
38    hasLiked: false,
39    thumbnail: ''
40  }
41];
42
43export default function App() {
44  const [currentIndex, setCurrentIndex] = useState(0);
45  const [videoStates, setVideoStates] = useState(videos);
46
47  const currentVideo = videoStates[currentIndex];
48
49  const handleScroll = (direction) => {
50    if (direction === 'down' && currentIndex < videos.length - 1) {
51      setCurrentIndex(currentIndex + 1);
52    } else if (direction === 'up' && currentIndex > 0) {
53      setCurrentIndex(currentIndex - 1);
54    }
55  };
56
57  const handleLike = () => {
58    setVideoStates(videoStates.map((v, i) =>
59      i === currentIndex
60        ? { ...v, likes: v.hasLiked ? v.likes - 1 : v.likes + 1, hasLiked: !v.hasLiked }
61        : v
62    ));
63  };
64
65  const formatCount = (count) => {
66    if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
67    if (count >= 1000) return `${(count / 1000).toFixed(1)}K`;
68    return count.toString();
69  };
70
71  return (
72    <div className="h-screen bg-black relative overflow-hidden">
73      {/* Conteneur de la vidéo */}
74      <div
75        className="absolute inset-0 transition-transform duration-300"
76        style={{ transform: `translateY(-${currentIndex * 100}%)` }}
77      >
78        {videoStates.map((video, index) => (
79          <div
80            key={video.id}
81            className="h-screen w-full relative flex items-center justify-center bg-gradient-to-br from-purple-900 to-blue-900"
82          >
83            {/* Emplacement de la vidéo */}
84            <div className="text-9xl">{video.thumbnail}</div>
85
86            {/* Superposition d'infos de la vidéo */}
87            {index === currentIndex && (
88              <>
89                {/* Actions à droite */}
90                <div className="absolute right-4 bottom-20 flex flex-col gap-6 items-center">
91                  {/* Avatar */}
92                  <div className="relative text-4xl">
93                    {video.author.avatar}
94                    {video.author.isVerified && (
95                      <div className="absolute -top-1 -right-1 w-4 h-4 bg-blue-500 rounded-full flex items-center justify-center text-white text-xs">
96                        √
97                      </div>
98                    )}
99                  </div>
100
101                  {/* J’aime */}
102                  <button onClick={handleLike} className="flex flex-col items-center gap-1">
103                    <div className={`text-sm font-semibold ${video.hasLiked ? 'text-red-500' : 'text-white'}`}>
104                      {video.hasLiked ? 'Aimé' : 'J’aime'}
105                    </div>
106                    <span className="text-white text-xs font-bold">
107                      {formatCount(video.likes)}
108                    </span>
109                  </button>
110
111                  {/* Commentaire */}
112                  <button className="flex flex-col items-center gap-1">
113                    <div className="text-sm font-semibold text-white">Commenter</div>
114                    <span className="text-white text-xs font-bold">
115                      {formatCount(video.comments)}
116                    </span>
117                  </button>
118
119                  {/* Partager */}
120                  <button className="flex flex-col items-center gap-1">
121                    <div className="text-sm font-semibold text-white">Partager</div>
122                    <span className="text-white text-xs font-bold">
123                      {formatCount(video.shares)}
124                    </span>
125                  </button>
126
127                  {/* Son */}
128                  <button className="w-10 h-10 rounded-full bg-gradient-to-r from-pink-500 to-purple-500 animate-spin-slow flex items-center justify-center text-xl">
129                    ◉
130                  </button>
131                </div>
132
133                {/* Infos du bas */}
134                <div className="absolute bottom-4 left-4 right-20">
135                  <div className="flex items-center gap-2 mb-2">
136                    <span className="text-white font-bold">@{video.author.username}</span>
137                    {video.author.isVerified && <span className="text-blue-400">√</span>}
138                  </div>
139
140                  <p className="text-white text-sm mb-2">{video.caption}</p>
141
142                  <div className="flex items-center gap-2 bg-black/40 px-3 py-1.5 rounded-full max-w-xs">
143                    <span className="text-white text-xs">Musique</span>
144                    <span className="text-white text-xs truncate">{video.sound.name}</span>
145                  </div>
146                </div>
147              </>
148            )}
149          </div>
150        ))}
151      </div>
152
153      {/* Navigation */}
154      <div className="absolute right-4 top-1/2 transform -translate-y-1/2 flex flex-col gap-4">
155        <button
156          onClick={() => handleScroll('up')}
157          disabled={currentIndex === 0}
158          className="w-12 h-12 bg-white/20 hover:bg-white/30 disabled:bg-white/10 rounded-full flex items-center justify-center text-white"
159        >
160          ▲
161        </button>
162        <button
163          onClick={() => handleScroll('down')}
164          disabled={currentIndex === videos.length - 1}
165          className="w-12 h-12 bg-white/20 hover:bg-white/30 disabled:bg-white/10 rounded-full flex items-center justify-center text-white"
166        >
167          ▼
168        </button>
169      </div>
170
171      {/* Progression */}
172      <div className="absolute top-4 right-4 text-white text-sm bg-black/50 px-3 py-1 rounded-full">
173        {currentIndex + 1} / {videos.length}
174      </div>
175    </div>
176  );
177}

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. Principale différence entre TikTok et YouTube ?

  2. 2. Qu'est-ce que la page Pour toi ?

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

Tâches pratiques dans le jeu

  • Éditeur de code

    Fil de style TikTok

  • Tri horizontal

    Ordonne la ligne qui déclare l'index de la vidéo actuelle dans le flux :

  • Clique dans l'ordre

    Construis l'appel qui lance la lecture de la vidéo actuelle (clique dans l'ordre) :

  • Éditeur de code

    Composant de superposition de la vidéo

  • Tri vertical

    Ordonne les actions sur le bord droit de la superposition (de haut en bas) :

  • Tri horizontal

    Ordonne le début du mapping des hashtags de la vidéo vers JSX :

Articles utiles