We use cookies to enhance your experience on the site
CodeWorlds

Categories, Clips, and Discovery

Twitch has discovery - browse by category, clips, VODs.

Browse Categories

1interface BrowseData {
2  liveChannels: Channel[];
3  categories: Category[];
4  clips: Clip[];
5  vods: VOD[];
6}
7
8// Category page
9'use client';
10
11export default function BrowsePage() {
12  const [activeTab, setActiveTab] = useState<'categories' | 'live' | 'clips'>('categories');
13
14  return (
15    <div className="min-h-screen bg-gray-950 text-white p-8">
16      <h1 className="text-3xl font-bold mb-6">Browse</h1>
17
18      {/* Tabs */}
19      <div className="flex gap-6 mb-8 border-b border-gray-800">
20        {[
21          { key: 'categories', label: 'Categories' },
22          { key: 'live', label: 'Live Channels' },
23          { key: 'clips', label: 'Clips' }
24        ].map(tab => (
25          <button
26            key={tab.key}
27            onClick={() => setActiveTab(tab.key as any)}
28            className={`
29              pb-3 font-semibold
30              ${activeTab === tab.key
31                ? 'border-b-2 border-purple-500'
32                : 'text-gray-400 hover:text-white'
33              }
34            `}
35          >
36            {tab.label}
37          </button>
38        ))}
39      </div>
40
41      {/* Content */}
42      {activeTab === 'categories' && (
43        <div className="grid grid-cols-6 gap-4">
44          {mockCategories.map(category => (
45            <CategoryCard key={category.id} category={category} />
46          ))}
47        </div>
48      )}
49
50      {activeTab === 'live' && (
51        <div className="grid grid-cols-4 gap-4">
52          {mockLiveChannels.map(channel => (
53            <LiveChannelCard key={channel.id} channel={channel} />
54          ))}
55        </div>
56      )}
57
58      {activeTab === 'clips' && (
59        <div className="grid grid-cols-4 gap-4">
60          {mockClips.map(clip => (
61            <ClipCard key={clip.id} clip={clip} />
62          ))}
63        </div>
64      )}
65    </div>
66  );
67}
68
69function CategoryCard({ category }: { category: Category }) {
70  return (
71    <div className="cursor-pointer group">
72      <div className="relative mb-2 overflow-hidden rounded">
73        <img
74          src={category.boxArtUrl}
75          alt={category.name}
76          className="w-full aspect-[3/4] object-cover group-hover:scale-110 transition"
77        />
78        <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black to-transparent p-2">
79          <div className="text-xs text-gray-300">
80            {category.viewerCount.toLocaleString()} viewers
81          </div>
82        </div>
83      </div>
84      <h3 className="font-semibold">{category.name}</h3>
85      <div className="flex flex-wrap gap-1 mt-1">
86        {category.tags.slice(0, 2).map(tag => (
87          <span key={tag} className="text-xs bg-gray-800 px-2 py-1 rounded">
88            {tag}
89          </span>
90        ))}
91      </div>
92    </div>
93  );
94}

Clips System

1interface Clip {
2  id: string;
3  streamId: string;
4  channelId: string;
5  channel: Channel;
6
7  // Content
8  title: string;
9  videoUrl: string;
10  thumbnailUrl: string;
11  duration: number; // seconds (max 60s)
12
13  // Metadata
14  createdBy: string;
15  createdAt: Date;
16  views: number;
17  category: Category;
18
19  // Source
20  streamTimestamp: number; // position in original stream
21}
22
23// Create clip from live stream
24function createClip(
25  streamId: string,
26  currentTimestamp: number,
27  duration: number = 30
28): Clip {
29  // Capture video from (currentTimestamp - duration/2) to (currentTimestamp + duration/2)
30  // Max 60 seconds
31
32  return {
33    id: generateId(),
34    streamId,
35    channelId: getCurrentChannel().id,
36    channel: getCurrentChannel(),
37    title: `${getCurrentChannel().displayName} - Clip`,
38    videoUrl: `/clips/${generateId()}.mp4`,
39    thumbnailUrl: `/clips/${generateId()}-thumb.jpg`,
40    duration: Math.min(duration, 60),
41    createdBy: getCurrentUser().id,
42    createdAt: new Date(),
43    views: 0,
44    category: getCurrentStream().category,
45    streamTimestamp: currentTimestamp
46  };
47}
48
49// Clip player
50'use client';
51
52export default function ClipPlayer({ clip }: { clip: Clip }) {
53  const [isPlaying, setIsPlaying] = useState(false);
54
55  return (
56    <div className="bg-gray-900 rounded overflow-hidden">
57      {/* Video */}
58      <div className="relative">
59        <video
60          src={clip.videoUrl}
61          poster={clip.thumbnailUrl}
62          className="w-full aspect-video"
63          controls
64          autoPlay={isPlaying}
65        />
66      </div>
67
68      {/* Info */}
69      <div className="p-4">
70        <div className="flex items-start gap-3 mb-3">
71          <img
72            src={clip.channel.avatar}
73            alt={clip.channel.displayName}
74            className="w-10 h-10 rounded-full"
75          />
76          <div className="flex-1">
77            <h3 className="font-semibold">{clip.title}</h3>
78            <p className="text-sm text-gray-400">{clip.channel.displayName}</p>
79          </div>
80        </div>
81
82        <div className="flex items-center gap-4 text-sm text-gray-400">
83          <span>{clip.views.toLocaleString()} views</span>
84          <span></span>
85          <span>{new Date(clip.createdAt).toLocaleDateString()}</span>
86          <span></span>
87          <span>{clip.category.name}</span>
88        </div>
89
90        {/* Actions */}
91        <div className="flex gap-2 mt-4">
92          <button className="flex-1 px-4 py-2 bg-gray-800 rounded hover:bg-gray-700">
93            Share
94          </button>
95          <button className="flex-1 px-4 py-2 bg-gray-800 rounded hover:bg-gray-700">
96            Watch Full Video
97          </button>
98        </div>
99      </div>
100    </div>
101  );
102}

VODs (Video on Demand)

1interface VOD {
2  id: string;
3  channelId: string;
4  channel: Channel;
5
6  // Content
7  title: string;
8  videoUrl: string;
9  thumbnailUrl: string;
10  duration: number; // seconds
11
12  // Metadata
13  streamedAt: Date;
14  views: number;
15  category: Category;
16
17  // Chapters (optional)
18  chapters?: Chapter[];
19}
20
21interface Chapter {
22  id: string;
23  title: string;
24  timestamp: number; // seconds from start
25  duration: number;
26}
27
28// VOD with chapters
29'use client';
30
31export default function VODPlayer({ vod }: { vod: VOD }) {
32  const [currentTime, setCurrentTime] = useState(0);
33  const [showChapters, setShowChapters] = useState(false);
34
35  const getCurrentChapter = () => {
36    if (!vod.chapters) return null;
37
38    return vod.chapters.find((chapter, i) => {
39      const nextChapter = vod.chapters![i + 1];
40      return currentTime >= chapter.timestamp &&
41        (!nextChapter || currentTime < nextChapter.timestamp);
42    });
43  };
44
45  const currentChapter = getCurrentChapter();
46
47  return (
48    <div className="bg-gray-900 rounded overflow-hidden">
49      <video
50        src={vod.videoUrl}
51        poster={vod.thumbnailUrl}
52        className="w-full aspect-video"
53        controls
54        onTimeUpdate={(e) => setCurrentTime(e.currentTarget.currentTime)}
55      />
56
57      {/* Chapter indicator */}
58      {currentChapter && (
59        <div className="bg-gray-800 px-4 py-2 text-sm">
60          📖 {currentChapter.title}
61        </div>
62      )}
63
64      {/* Chapters list */}
65      {vod.chapters && (
66        <div>
67          <button
68            onClick={() => setShowChapters(!showChapters)}
69            className="w-full px-4 py-2 bg-gray-800 text-left hover:bg-gray-700"
70          >
71            Chapters ({vod.chapters.length})
72          </button>
73
74          {showChapters && (
75            <div className="max-h-60 overflow-y-auto">
76              {vod.chapters.map(chapter => (
77                <button
78                  key={chapter.id}
79                  className={`
80                    w-full px-4 py-2 text-left hover:bg-gray-800
81                    ${currentChapter?.id === chapter.id ? 'bg-gray-800' : ''}
82                  `}
83                >
84                  <div className="flex items-center gap-3">
85                    <span className="text-sm text-gray-400">
86                      {formatTime(chapter.timestamp)}
87                    </span>
88                    <span className="flex-1">{chapter.title}</span>
89                  </div>
90                </button>
91              ))}
92            </div>
93          )}
94        </div>
95      )}
96    </div>
97  );
98}
99
100function formatTime(seconds: number): string {
101  const h = Math.floor(seconds / 3600);
102  const m = Math.floor((seconds % 3600) / 60);
103  const s = Math.floor(seconds % 60);
104
105  if (h > 0) {
106    return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
107  }
108  return `${m}:${s.toString().padStart(2, '0')}`;
109}

Summary 🎓

Browse categories - box art, viewer count, tags ✅ Live channels grid - active streams by category ✅ Clips system - create clips from streams (max 60s) ✅ Clip player - play clip, view info, share ✅ VODs - recorded streams ✅ VOD chapters - timestamp navigation ✅ Category cards - visual discovery ✅ Trending clips - most viewed clips

You now have a complete Twitch clone with live streaming, chat, subscriptions, clips, and VODs! 📺

See you soon! 🚀

Go to CodeWorlds