Cours Vibe coding avec l'IA · Module 13: Clone de Twitch
Chaîne et abonnements
Dans cette leçon3
Twitch, c'est une histoire de monétisation - abonnements, bits, followers.
Système d'abonnement
1interface Subscription {
2 userId: string;
3 channelId: string;
4 tier: 'tier1' | 'tier2' | 'tier3'; // $4.99, $9.99, $24.99
5 startDate: Date;
6 renewDate: Date;
7 isGift: boolean;
8 giftedBy?: string;
9 streak: number; // consecutive months
10 cumulativeMonths: number; // total months
11}
12
13interface SubBenefits {
14 tier1: {
15 price: 4.99;
16 emotes: string[]; // unlock emotes
17 badge: string;
18 adFree: true;
19 chatPerks: {
20 slowModeExempt: true;
21 subsOnlyChat: true;
22 };
23 };
24 tier2: {
25 price: 9.99;
26 emotes: string[];
27 badge: string;
28 adFree: true;
29 chatPerks: {
30 slowModeExempt: true;
31 subsOnlyChat: true;
32 };
33 };
34 tier3: {
35 price: 24.99;
36 emotes: string[];
37 badge: string;
38 adFree: true;
39 chatPerks: {
40 slowModeExempt: true;
41 subsOnlyChat: true;
42 };
43 };
44}
45
46interface Bits {
47 userId: string;
48 channelId: string;
49 amount: number; // 100 bits = $1
50 message?: string;
51 timestamp: Date;
52}Page de chaîne
1'use client';
2
3interface ChannelPageProps {
4 channel: Channel;
5 isFollowing: boolean;
6 isSubscribed: boolean;
7 subscriptionTier?: 'tier1' | 'tier2' | 'tier3';
8}
9
10export default function ChannelPage({
11 channel,
12 isFollowing,
13 isSubscribed,
14 subscriptionTier
15}: ChannelPageProps) {
16 const [activeTab, setActiveTab] = useState<'home' | 'videos' | 'schedule' | 'about'>('home');
17
18 return (
19 <div className="min-h-screen bg-gray-950 text-white">
20 {/* Banner */}
21 {channel.banner && (
22 <div
23 className="h-80 bg-cover bg-center"
24 style={{ backgroundImage: `url(${channel.banner})` }}
25 />
26 )}
27
28 {/* Channel Header */}
29 <div className="max-w-7xl mx-auto px-6 -mt-16 relative z-10">
30 <div className="flex items-end gap-6 mb-6">
31 {/* Avatar */}
32 <img
33 src={channel.avatar}
34 alt={channel.displayName}
35 className="w-32 h-32 rounded-full border-4 border-gray-950"
36 />
37
38 {/* Info */}
39 <div className="flex-1 pb-4">
40 <h1 className="text-4xl font-bold mb-2">{channel.displayName}</h1>
41 <p className="text-gray-400">{channel.description}</p>
42 </div>
43
44 {/* Actions */}
45 <div className="flex items-center gap-3 pb-4">
46 <button
47 className={`
48 px-6 py-2 rounded font-semibold
49 ${isFollowing
50 ? 'bg-gray-700 hover:bg-gray-600'
51 : 'bg-purple-600 hover:bg-purple-700'
52 }
53 `}
54 >
55 {isFollowing ? 'Following' : 'Follow'}
56 </button>
57
58 {!isSubscribed ? (
59 <button className="px-6 py-2 bg-purple-600 rounded font-semibold hover:bg-purple-700">
60 Subscribe
61 </button>
62 ) : (
63 <button className="px-6 py-2 bg-purple-900 border border-purple-600 rounded font-semibold hover:bg-purple-800">
64 Subscribed ({subscriptionTier})
65 </button>
66 )}
67
68 <button className="p-2 hover:bg-gray-800 rounded">
69 Paramètres
70 </button>
71 </div>
72 </div>
73
74 {/* Stats */}
75 <div className="flex gap-6 mb-6 text-sm text-gray-400">
76 <div>
77 <span className="font-semibold text-white">{channel.followers.toLocaleString()}</span> followers
78 </div>
79 <div>
80 <span className="font-semibold text-white">{channel.totalViews.toLocaleString()}</span> total views
81 </div>
82 {channel.isPartner && (
83 <div className="flex items-center gap-1">
84 <span className="text-purple-500">√</span>
85 <span>Partner</span>
86 </div>
87 )}
88 </div>
89
90 {/* Tabs */}
91 <div className="border-b border-gray-800">
92 <div className="flex gap-8">
93 {[
94 { key: 'home', label: 'Home' },
95 { key: 'videos', label: 'Videos' },
96 { key: 'schedule', label: 'Schedule' },
97 { key: 'about', label: 'About' }
98 ].map(tab => (
99 <button
100 key={tab.key}
101 onClick={() => setActiveTab(tab.key as any)}
102 className={`
103 pb-3 font-semibold
104 ${activeTab === tab.key
105 ? 'border-b-2 border-purple-500 text-white'
106 : 'text-gray-400 hover:text-white'
107 }
108 `}
109 >
110 {tab.label}
111 </button>
112 ))}
113 </div>
114 </div>
115 </div>
116
117 {/* Content */}
118 <div className="max-w-7xl mx-auto px-6 py-8">
119 {activeTab === 'home' && (
120 <div>
121 {/* Live Stream or Offline */}
122 {channel.isLive && channel.currentStream ? (
123 <div className="mb-8">
124 <h2 className="text-xl font-bold mb-4">Live Now</h2>
125 <StreamCard stream={channel.currentStream} />
126 </div>
127 ) : (
128 <div className="text-center py-12 text-gray-500">
129 <div className="text-4xl mb-4">◆</div>
130 <p className="text-lg">Channel is offline</p>
131 <p>Check back later or follow to get notified</p>
132 </div>
133 )}
134 </div>
135 )}
136
137 {activeTab === 'videos' && (
138 <div>
139 <h2 className="text-xl font-bold mb-4">Recent Videos</h2>
140 <div className="grid grid-cols-3 gap-4">
141 {/* VODs grid */}
142 </div>
143 </div>
144 )}
145
146 {activeTab === 'about' && (
147 <div className="max-w-2xl">
148 <h2 className="text-xl font-bold mb-4">About</h2>
149 <p className="text-gray-300 leading-relaxed">
150 {channel.description}
151 </p>
152 </div>
153 )}
154 </div>
155 </div>
156 );
157}
158
159function StreamCard({ stream }: { stream: Stream }) {
160 return (
161 <div className="bg-gray-900 rounded overflow-hidden hover:scale-105 transition cursor-pointer">
162 <div className="relative">
163 <img
164 src={stream.thumbnailUrl}
165 alt={stream.title}
166 className="w-full aspect-video object-cover"
167 />
168 <div className="absolute top-2 left-2 px-2 py-1 bg-red-600 text-white text-xs font-bold rounded">
169 LIVE
170 </div>
171 <div className="absolute bottom-2 left-2 px-2 py-1 bg-black/70 text-white text-xs rounded">
172 {stream.viewerCount.toLocaleString()} viewers
173 </div>
174 </div>
175
176 <div className="p-3">
177 <h3 className="font-semibold mb-1 line-clamp-1">{stream.title}</h3>
178 <p className="text-sm text-gray-400">{stream.category.name}</p>
179 </div>
180 </div>
181 );
182}Récapitulatif
Page de chaîne - bannière, avatar, description, onglets Bouton Follow - suivre/ne plus suivre une chaîne Bouton Subscribe - paliers d'abonnement Statistiques de la chaîne - followers, nombre total de vues Badge partenaire - statut de partenaire vérifié Carte de stream en direct - miniature, spectateurs, catégorie État hors ligne - message quand la chaîne ne diffuse pas Paliers d'abonnement - tier1, tier2, tier3 avec leurs avantages
Dans l'exercice suivant, on ajoutera la navigation par catégories et les clips !
À bientôt !
Code de cette leçon : App.tsx
1'use client';
2
3import { useState } from 'react';
4
5interface ChatMessage {
6 id: string;
7 username: string;
8 displayName: string;
9 badges: string[];
10 message: string;
11 timestamp: Date;
12}
13
14const mockMessages: ChatMessage[] = [
15 {
16 id: '1',
17 username: 'captain_redbeard',
18 displayName: 'Capitaine Barbe-Rouge',
19 badges: ['broadcaster'],
20 message: 'Ohé les pirates ! Bienvenue sur le stream !',
21 timestamp: new Date()
22 },
23 {
24 id: '2',
25 username: 'sailor_sam',
26 displayName: 'Matelot Sam',
27 badges: ['subscriber'],
28 message: 'PogChamp stream incroyable !',
29 timestamp: new Date()
30 },
31 {
32 id: '3',
33 username: 'navigator_nancy',
34 displayName: 'Navigatrice Nancy',
35 badges: ['moderator'],
36 message: 'Respectez les règles du chat ! Soyez sympas :)',
37 timestamp: new Date()
38 }
39];
40
41const badges = {
42 broadcaster: '',
43 moderator: '',
44 subscriber: '',
45 vip: ''
46};
47
48export default function LiveChat() {
49 const [messages, setMessages] = useState(mockMessages);
50 const [inputValue, setInputValue] = useState('');
51 const [showEmotes, setShowEmotes] = useState(false);
52
53 const sendMessage = () => {
54 if (!inputValue.trim()) return;
55
56 const newMessage: ChatMessage = {
57 id: Date.now().toString(),
58 username: 'you',
59 displayName: 'Toi',
60 badges: [],
61 message: inputValue,
62 timestamp: new Date()
63 };
64
65 setMessages([...messages, newMessage]);
66 setInputValue('');
67 };
68
69 return (
70 <div className="min-h-screen bg-black flex items-center justify-center p-4">
71 <div className="w-80 h-[600px] bg-gray-900 flex flex-col rounded-lg overflow-hidden">
72 {/* Header */}
73 <div className="p-3 border-b border-gray-800 bg-gray-950">
74 <h3 className="text-white font-semibold">STREAM CHAT</h3>
75 </div>
76
77 {/* Messages */}
78 <div className="flex-1 overflow-y-auto p-3 space-y-2">
79 {messages.map(msg => (
80 <div key={msg.id} className="text-sm">
81 {/* Badges */}
82 {msg.badges.map(badge => (
83 <span key={badge} className="mr-1" title={badge}>
84 {badges[badge as keyof typeof badges]}
85 </span>
86 ))}
87
88 {/* Username */}
89 <span className="font-semibold text-purple-400">
90 {msg.displayName}:
91 </span>
92
93 {/* Message */}
94 <span className="text-white ml-1">{msg.message}</span>
95 </div>
96 ))}
97 </div>
98
99 {/* Input */}
100 <div className="p-3 border-t border-gray-800">
101 <div className="flex gap-2">
102 <button
103 onClick={() => setShowEmotes(!showEmotes)}
104 className="p-2 text-gray-400 hover:text-white bg-gray-800 rounded"
105 >
106 :)
107 </button>
108
109 <input
110 type="text"
111 value={inputValue}
112 onChange={(e) => setInputValue(e.target.value)}
113 onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
114 placeholder="Send a message"
115 className="flex-1 bg-gray-800 text-white px-3 py-2 rounded outline-none focus:ring-2 focus:ring-purple-500"
116 />
117
118 <button
119 onClick={sendMessage}
120 disabled={!inputValue.trim()}
121 className="px-4 py-2 bg-purple-600 text-white rounded font-semibold hover:bg-purple-700 disabled:bg-gray-700"
122 >
123 Chat
124 </button>
125 </div>
126 </div>
127 </div>
128 </div>
129 );
130}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. Quels paliers d'abonnement Twitch propose-t-il ?
2. Combien de dollars valent 100 bits sur Twitch ?
Voici 2 des 6 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Clique dans l'ordre
Construis l'affichage du nombre d'abonnés formaté : channel.followers.toLocaleString()
- Tri vertical
Ordonne les sections de la page de chaîne depuis le haut de la page :
- Tri vertical
Ordonne les paliers d'abonnement du moins cher au plus cher :
- Éditeur de code
Construis une page de chaîne de streamer
- Clique dans l'ordre
Construis le changement de l'onglet actif vers 'home' : setActiveTab('home')
- Éditeur de code
Crée une carte de stream en direct