Cours Vibe coding avec l'IA · Module 10: Clone de TikTok
Fil de vidéos et page Pour toi
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. Principale différence entre TikTok et YouTube ?
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 :