Cours Vibe coding avec l'IA · Module 13: Clone de Twitch

Chaîne et abonnements

4 min de lecture
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. 1. Quels paliers d'abonnement Twitch propose-t-il ?

  2. 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

Articles utiles