Vibe coding with AI course Β· Module 13: Twitch Clone
Channel and Subscriptions
In this lesson3
Twitch is about monetization - subscriptions, bits, followers.
Subscription System
interface Subscription {
userId: string;
channelId: string;
tier: 'tier1' | 'tier2' | 'tier3'; // $4.99, $9.99, $24.99
startDate: Date;
renewDate: Date;
isGift: boolean;
giftedBy?: string;
streak: number; // consecutive months
cumulativeMonths: number; // total months
}
interface SubBenefits {
tier1: {
price: 4.99;
emotes: string[]; // unlock emotes
badge: string;
adFree: true;
chatPerks: {
slowModeExempt: true;
subsOnlyChat: true;
};
};
tier2: {
price: 9.99;
emotes: string[];
badge: string;
adFree: true;
chatPerks: {
slowModeExempt: true;
subsOnlyChat: true;
};
};
tier3: {
price: 24.99;
emotes: string[];
badge: string;
adFree: true;
chatPerks: {
slowModeExempt: true;
subsOnlyChat: true;
};
};
}
interface Bits {
userId: string;
channelId: string;
amount: number; // 100 bits = $1
message?: string;
timestamp: Date;
}Channel Page
'use client';
interface ChannelPageProps {
channel: Channel;
isFollowing: boolean;
isSubscribed: boolean;
subscriptionTier?: 'tier1' | 'tier2' | 'tier3';
}
export default function ChannelPage({
channel,
isFollowing,
isSubscribed,
subscriptionTier
}: ChannelPageProps) {
const [activeTab, setActiveTab] = useState<'home' | 'videos' | 'schedule' | 'about'>('home');
return (
<div className="min-h-screen bg-gray-950 text-white">
{/* Banner */}
{channel.banner && (
<div
className="h-80 bg-cover bg-center"
style={{ backgroundImage: `url(${channel.banner})` }}
/>
)}
{/* Channel Header */}
<div className="max-w-7xl mx-auto px-6 -mt-16 relative z-10">
<div className="flex items-end gap-6 mb-6">
{/* Avatar */}
<img
src={channel.avatar}
alt={channel.displayName}
className="w-32 h-32 rounded-full border-4 border-gray-950"
/>
{/* Info */}
<div className="flex-1 pb-4">
<h1 className="text-4xl font-bold mb-2">{channel.displayName}</h1>
<p className="text-gray-400">{channel.description}</p>
</div>
{/* Actions */}
<div className="flex items-center gap-3 pb-4">
<button
className={`
px-6 py-2 rounded font-semibold
${isFollowing
? 'bg-gray-700 hover:bg-gray-600'
: 'bg-purple-600 hover:bg-purple-700'
}
`}
>
{isFollowing ? 'Following' : 'Follow'}
</button>
{!isSubscribed ? (
<button className="px-6 py-2 bg-purple-600 rounded font-semibold hover:bg-purple-700">
Subscribe
</button>
) : (
<button className="px-6 py-2 bg-purple-900 border border-purple-600 rounded font-semibold hover:bg-purple-800">
Subscribed ({subscriptionTier})
</button>
)}
<button className="p-2 hover:bg-gray-800 rounded">
Settings
</button>
</div>
</div>
{/* Stats */}
<div className="flex gap-6 mb-6 text-sm text-gray-400">
<div>
<span className="font-semibold text-white">{channel.followers.toLocaleString()}</span> followers
</div>
<div>
<span className="font-semibold text-white">{channel.totalViews.toLocaleString()}</span> total views
</div>
{channel.isPartner && (
<div className="flex items-center gap-1">
<span className="text-purple-500">β</span>
<span>Partner</span>
</div>
)}
</div>
{/* Tabs */}
<div className="border-b border-gray-800">
<div className="flex gap-8">
{[
{ key: 'home', label: 'Home' },
{ key: 'videos', label: 'Videos' },
{ key: 'schedule', label: 'Schedule' },
{ key: 'about', label: 'About' }
].map(tab => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key as any)}
className={`
pb-3 font-semibold
${activeTab === tab.key
? 'border-b-2 border-purple-500 text-white'
: 'text-gray-400 hover:text-white'
}
`}
>
{tab.label}
</button>
))}
</div>
</div>
</div>
{/* Content */}
<div className="max-w-7xl mx-auto px-6 py-8">
{activeTab === 'home' && (
<div>
{/* Live Stream or Offline */}
{channel.isLive && channel.currentStream ? (
<div className="mb-8">
<h2 className="text-xl font-bold mb-4">Live Now</h2>
<StreamCard stream={channel.currentStream} />
</div>
) : (
<div className="text-center py-12 text-gray-500">
<div className="text-4xl mb-4">β</div>
<p className="text-lg">Channel is offline</p>
<p>Check back later or follow to get notified</p>
</div>
)}
</div>
)}
{activeTab === 'videos' && (
<div>
<h2 className="text-xl font-bold mb-4">Recent Videos</h2>
<div className="grid grid-cols-3 gap-4">
{/* VODs grid */}
</div>
</div>
)}
{activeTab === 'about' && (
<div className="max-w-2xl">
<h2 className="text-xl font-bold mb-4">About</h2>
<p className="text-gray-300 leading-relaxed">
{channel.description}
</p>
</div>
)}
</div>
</div>
);
}
function StreamCard({ stream }: { stream: Stream }) {
return (
<div className="bg-gray-900 rounded overflow-hidden hover:scale-105 transition cursor-pointer">
<div className="relative">
<img
src={stream.thumbnailUrl}
alt={stream.title}
className="w-full aspect-video object-cover"
/>
<div className="absolute top-2 left-2 px-2 py-1 bg-red-600 text-white text-xs font-bold rounded">
LIVE
</div>
<div className="absolute bottom-2 left-2 px-2 py-1 bg-black/70 text-white text-xs rounded">
{stream.viewerCount.toLocaleString()} viewers
</div>
</div>
<div className="p-3">
<h3 className="font-semibold mb-1 line-clamp-1">{stream.title}</h3>
<p className="text-sm text-gray-400">{stream.category.name}</p>
</div>
</div>
);
}Summary
Channel page - banner, avatar, description, tabs Follow button - follow/unfollow channel Subscribe button - subscription tiers Channel stats - followers, total views Partner badge - verified partner status Live stream card - thumbnail, viewer count, category Offline state - message when not streaming Subscription tiers - tier1, tier2, tier3 with benefits
In the next exercise we'll add browse categories and clips!
See you soon!
Code for this lesson: App.tsx
'use client';
import { useState } from 'react';
interface ChatMessage {
id: string;
username: string;
displayName: string;
badges: string[];
message: string;
timestamp: Date;
}
const mockMessages: ChatMessage[] = [
{
id: '1',
username: 'captain_redbeard',
displayName: 'Captain Redbeard',
badges: ['broadcaster'],
message: 'Ahoj pirates! Welcome to the stream!',
timestamp: new Date()
},
{
id: '2',
username: 'sailor_sam',
displayName: 'Sailor Sam',
badges: ['subscriber'],
message: 'PogChamp amazing stream!',
timestamp: new Date()
},
{
id: '3',
username: 'navigator_nancy',
displayName: 'Navigator Nancy',
badges: ['moderator'],
message: 'Remember chat rules! Be nice :)',
timestamp: new Date()
}
];
const badges = {
broadcaster: '',
moderator: '',
subscriber: '',
vip: ''
};
export default function LiveChat() {
const [messages, setMessages] = useState(mockMessages);
const [inputValue, setInputValue] = useState('');
const [showEmotes, setShowEmotes] = useState(false);
const sendMessage = () => {
if (!inputValue.trim()) return;
const newMessage: ChatMessage = {
id: Date.now().toString(),
username: 'you',
displayName: 'You',
badges: [],
message: inputValue,
timestamp: new Date()
};
setMessages([...messages, newMessage]);
setInputValue('');
};
return (
<div className="min-h-screen bg-black flex items-center justify-center p-4">
<div className="w-80 h-[600px] bg-gray-900 flex flex-col rounded-lg overflow-hidden">
{/* Header */}
<div className="p-3 border-b border-gray-800 bg-gray-950">
<h3 className="text-white font-semibold">STREAM CHAT</h3>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-3 space-y-2">
{messages.map(msg => (
<div key={msg.id} className="text-sm">
{/* Badges */}
{msg.badges.map(badge => (
<span key={badge} className="mr-1" title={badge}>
{badges[badge as keyof typeof badges]}
</span>
))}
{/* Username */}
<span className="font-semibold text-purple-400">
{msg.displayName}:
</span>
{/* Message */}
<span className="text-white ml-1">{msg.message}</span>
</div>
))}
</div>
{/* Input */}
<div className="p-3 border-t border-gray-800">
<div className="flex gap-2">
<button
onClick={() => setShowEmotes(!showEmotes)}
className="p-2 text-gray-400 hover:text-white bg-gray-800 rounded"
>
:)
</button>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
placeholder="Send a message"
className="flex-1 bg-gray-800 text-white px-3 py-2 rounded outline-none focus:ring-2 focus:ring-purple-500"
/>
<button
onClick={sendMessage}
disabled={!inputValue.trim()}
className="px-4 py-2 bg-purple-600 text-white rounded font-semibold hover:bg-purple-700 disabled:bg-gray-700"
>
Chat
</button>
</div>
</div>
</div>
</div>
);
}Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What subscription tiers does Twitch have?
2. How many dollars is 100 bits on Twitch?
These are 2 of 6 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Click in order
Build the display of the formatted follower count: channel.followers.toLocaleString()
- Vertical ordering
Arrange the channel page sections from the top of the page:
- Vertical ordering
Order the subscription tiers from cheapest to most expensive:
- Code editor
Build a streamer channel page
- Click in order
Build the active tab change to 'home': setActiveTab('home')
- Code editor
Create a live stream card