Vibe coding with AI course Β· Module 10: TikTok Clone

Video Feed and For You Page

5 min read
In this lesson5

Welcome! Time to create Treasure Reels - a platform for short pirate videos!

This will be like TikTok - vertical short videos, For You algorithm, sounds, effects, and duet/stitch features.

TikTok vs YouTube/Instagram

YouTube:

  • Long-form horizontal video (10+ min)
  • Subscriptions and recommended
  • Comments under videos

Instagram Reels:

  • Short vertical video in Stories/Reels tab
  • Mainly photos + video as an addition
  • Grid layout

TikTok:

  • Pure short vertical video (15-60s)
  • For You Page algorithm (highly personalized)
  • Sounds library and audio trends
  • Duet, Stitch, Effects
  • Fullscreen immersive feed

Video Structure

interface Video {
  id: string;
  authorId: string;
  author: {
    id: string;
    username: string;
    avatar: string;
    isVerified: boolean;
  };

  // Video content
  videoUrl: string;
  thumbnail: string;
  duration: number; // seconds

  // Caption
  caption: string;
  hashtags: string[]; // #treasurehunt, #piratelife

  // Audio
  sound: Sound;

  // Engagement
  likes: number;
  comments: number;
  shares: number;
  views: number;

  // User engagement
  hasLiked: boolean;
  hasBookmarked: boolean;
  isFollowingAuthor: boolean;

  createdAt: Date;
}

interface Sound {
  id: string;
  name: string; // "Pirate's Anthem - Redbeard"
  author: string;
  duration: number;
  useCount: number; // how many videos use this sound
  thumbnailUrl?: string;
}

For You Feed

'use client';

import { useState, useRef, useEffect } from 'react';

interface FeedProps {
  videos: Video[];
  onLike: (videoId: string) => void;
  onFollow: (authorId: string) => void;
  onComment: (videoId: string) => void;
  onShare: (videoId: string) => void;
}

export default function ForYouFeed({
  videos,
  onLike,
  onFollow,
  onComment,
  onShare
}: FeedProps) {
  const [currentIndex, setCurrentIndex] = useState(0);
  const [isPlaying, setIsPlaying] = useState(true);
  const videoRefs = useRef<(HTMLVideoElement | null)[]>([]);

  const currentVideo = videos[currentIndex];

  // Auto-play current video
  useEffect(() => {
    const video = videoRefs.current[currentIndex];
    if (video) {
      if (isPlaying) {
        video.play();
      } else {
        video.pause();
      }
    }
  }, [currentIndex, isPlaying]);

  const handleScroll = (direction: 'up' | 'down') => {
    if (direction === 'down' && currentIndex < videos.length - 1) {
      setCurrentIndex(currentIndex + 1);
    } else if (direction === 'up' && currentIndex > 0) {
      setCurrentIndex(currentIndex - 1);
    }
  };

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'ArrowUp') handleScroll('up');
    if (e.key === 'ArrowDown') handleScroll('down');
    if (e.key === ' ') {
      e.preventDefault();
      setIsPlaying(!isPlaying);
    }
  };

  return (
    <div
      className="h-screen overflow-hidden bg-black relative"
      onKeyDown={handleKeyDown}
      tabIndex={0}
    >
      {/* Video Container */}
      <div
        className="absolute inset-0 transition-transform duration-300"
        style={{
          transform: `translateY(-${currentIndex * 100}%)`
        }}
      >
        {videos.map((video, index) => (
          <div
            key={video.id}
            className="h-screen w-full relative flex items-center justify-center"
          >
            {/* Video */}
            <video
              ref={el => videoRefs.current[index] = el}
              src={video.videoUrl}
              className="h-full w-auto max-w-full object-contain"
              loop
              playsInline
              onClick={() => setIsPlaying(!isPlaying)}
            />

            {/* Play/Pause Indicator */}
            {!isPlaying && index === currentIndex && (
              <div className="absolute inset-0 flex items-center justify-center">
                <div className="w-20 h-20 bg-black/50 rounded-full flex items-center justify-center">
                  <span className="text-white text-4xl">β–Ά</span>
                </div>
              </div>
            )}

            {/* Video Info Overlay */}
            {index === currentIndex && (
              <VideoInfoOverlay
                video={video}
                onLike={() => onLike(video.id)}
                onFollow={() => onFollow(video.author.id)}
                onComment={() => onComment(video.id)}
                onShare={() => onShare(video.id)}
              />
            )}
          </div>
        ))}
      </div>

      {/* Navigation Arrows */}
      <div className="absolute right-4 top-1/2 transform -translate-y-1/2 flex flex-col gap-4">
        <button
          onClick={() => handleScroll('up')}
          disabled={currentIndex === 0}
          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"
        >
          β–²
        </button>
        <button
          onClick={() => handleScroll('down')}
          disabled={currentIndex === videos.length - 1}
          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"
        >
          β–Ό
        </button>
      </div>

      {/* Progress Indicator */}
      <div className="absolute top-4 right-4 text-white text-sm bg-black/50 px-3 py-1 rounded-full">
        {currentIndex + 1} / {videos.length}
      </div>
    </div>
  );
}

Video Info Overlay

interface VideoInfoOverlayProps {
  video: Video;
  onLike: () => void;
  onFollow: () => void;
  onComment: () => void;
  onShare: () => void;
}

function VideoInfoOverlay({
  video,
  onLike,
  onFollow,
  onComment,
  onShare
}: VideoInfoOverlayProps) {
  return (
    <>
      {/* Right Side Actions */}
      <div className="absolute right-4 bottom-20 flex flex-col gap-6 items-center">
        {/* Author Avatar */}
        <div className="relative">
          <img
            src={video.author.avatar}
            alt={video.author.username}
            className="w-12 h-12 rounded-full border-2 border-white"
          />
          {!video.isFollowingAuthor && (
            <button
              onClick={onFollow}
              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"
            >
              +
            </button>
          )}
          {video.author.isVerified && (
            <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">
              √
            </div>
          )}
        </div>

        {/* Like */}
        <button onClick={onLike} className="flex flex-col items-center gap-1">
          <div
            className={`text-sm font-semibold transition ${
              video.hasLiked ? 'text-red-500' : 'text-white'
            }`}
          >
            {video.hasLiked ? 'Liked' : 'Like'}
          </div>
          <span className="text-white text-xs font-semibold">
            {formatCount(video.likes)}
          </span>
        </button>

        {/* Comment */}
        <button onClick={onComment} className="flex flex-col items-center gap-1">
          <div className="text-sm font-semibold text-white">Comment</div>
          <span className="text-white text-xs font-semibold">
            {formatCount(video.comments)}
          </span>
        </button>

        {/* Bookmark */}
        <button className="flex flex-col items-center gap-1">
          <div
            className={`text-sm font-semibold transition ${
              video.hasBookmarked ? 'text-yellow-400' : 'text-white'
            }`}
          >
            {video.hasBookmarked ? 'Saved' : 'Save'}
          </div>
        </button>

        {/* Share */}
        <button onClick={onShare} className="flex flex-col items-center gap-1">
          <div className="text-sm font-semibold text-white">Share</div>
          <span className="text-white text-xs font-semibold">
            {formatCount(video.shares)}
          </span>
        </button>

        {/* Sound */}
        <button className="w-10 h-10 rounded-full overflow-hidden border-2 border-white animate-spin-slow">
          <img
            src={video.sound.thumbnailUrl || video.author.avatar}
            alt={video.sound.name}
            className="w-full h-full object-cover"
          />
        </button>
      </div>

      {/* Bottom Info */}
      <div className="absolute bottom-4 left-4 right-20">
        {/* Author */}
        <div className="flex items-center gap-2 mb-2">
          <span className="text-white font-bold">@{video.author.username}</span>
          {video.author.isVerified && (
            <span className="text-blue-400">√</span>
          )}
        </div>

        {/* Caption */}
        <p className="text-white text-sm mb-2 line-clamp-2">
          {video.caption}
        </p>

        {/* Hashtags */}
        {video.hashtags.length > 0 && (
          <div className="flex flex-wrap gap-2 mb-3">
            {video.hashtags.map((tag, idx) => (
              <span key={idx} className="text-white text-sm font-semibold">
                #{tag}
              </span>
            ))}
          </div>
        )}

        {/* Sound */}
        <div className="flex items-center gap-2 bg-black/40 px-3 py-1.5 rounded-full max-w-xs">
          <span className="text-white text-xs">Music</span>
          <span className="text-white text-xs truncate">
            {video.sound.name}
          </span>
        </div>
      </div>
    </>
  );
}

function formatCount(count: number): string {
  if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
  if (count >= 1000) return `${(count / 1000).toFixed(1)}K`;
  return count.toString();
}

Summary

Vertical video feed - fullscreen immersive scroll For You algorithm - personalized content Video overlay - author, caption, hashtags, sound Right side actions - like, comment, bookmark, share Scroll navigation - swipe up/down between videos Auto-play - video plays automatically when visible Play/pause - tap video to toggle Sound indicator - spinning disc with audio info Follow button - quick follow from avatar

In the next exercise we'll add sounds library and video creation!

See you soon!

Code for this lesson: App.jsx
// Vertical Video Feed
import { useState } from 'react';

const videos = [
  {
    id: 1,
    author: { username: 'CaptainJack', avatar: '', isVerified: true },
    caption: 'Found the treasure! #TreasureHunt #PirateLife',
    hashtags: ['TreasureHunt', 'PirateLife'],
    likes: 125000,
    comments: 3400,
    shares: 890,
    sound: { name: "Pirate's Anthem - Redbeard" },
    hasLiked: false,
    thumbnail: ''
  },
  {
    id: 2,
    author: { username: 'BlackbeardCrew', avatar: '', isVerified: false },
    caption: 'Epic battle at sea! #SeaBattle #Pirates',
    hashtags: ['SeaBattle', 'Pirates'],
    likes: 89000,
    comments: 2100,
    shares: 560,
    sound: { name: 'Battle Theme - Morgan' },
    hasLiked: false,
    thumbnail: ''
  },
  {
    id: 3,
    author: { username: 'TreasureHunter', avatar: '', isVerified: true },
    caption: 'Map to Isla de Muerta #Map #SecretIsland',
    hashtags: ['Map', 'SecretIsland'],
    likes: 234000,
    comments: 5600,
    shares: 1200,
    sound: { name: 'Adventure Awaits - Elizabeth' },
    hasLiked: false,
    thumbnail: ''
  }
];

export default function App() {
  const [currentIndex, setCurrentIndex] = useState(0);
  const [videoStates, setVideoStates] = useState(videos);

  const currentVideo = videoStates[currentIndex];

  const handleScroll = (direction) => {
    if (direction === 'down' && currentIndex < videos.length - 1) {
      setCurrentIndex(currentIndex + 1);
    } else if (direction === 'up' && currentIndex > 0) {
      setCurrentIndex(currentIndex - 1);
    }
  };

  const handleLike = () => {
    setVideoStates(videoStates.map((v, i) =>
      i === currentIndex
        ? { ...v, likes: v.hasLiked ? v.likes - 1 : v.likes + 1, hasLiked: !v.hasLiked }
        : v
    ));
  };

  const formatCount = (count) => {
    if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
    if (count >= 1000) return `${(count / 1000).toFixed(1)}K`;
    return count.toString();
  };

  return (
    <div className="h-screen bg-black relative overflow-hidden">
      {/* Video Container */}
      <div
        className="absolute inset-0 transition-transform duration-300"
        style={{ transform: `translateY(-${currentIndex * 100}%)` }}
      >
        {videoStates.map((video, index) => (
          <div
            key={video.id}
            className="h-screen w-full relative flex items-center justify-center bg-gradient-to-br from-purple-900 to-blue-900"
          >
            {/* Video Placeholder */}
            <div className="text-9xl">{video.thumbnail}</div>

            {/* Video Info Overlay */}
            {index === currentIndex && (
              <>
                {/* Right Actions */}
                <div className="absolute right-4 bottom-20 flex flex-col gap-6 items-center">
                  {/* Avatar */}
                  <div className="relative text-4xl">
                    {video.author.avatar}
                    {video.author.isVerified && (
                      <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">
                        √
                      </div>
                    )}
                  </div>

                  {/* Like */}
                  <button onClick={handleLike} className="flex flex-col items-center gap-1">
                    <div className={`text-sm font-semibold ${video.hasLiked ? 'text-red-500' : 'text-white'}`}>
                      {video.hasLiked ? 'Liked' : 'Like'}
                    </div>
                    <span className="text-white text-xs font-bold">
                      {formatCount(video.likes)}
                    </span>
                  </button>

                  {/* Comment */}
                  <button className="flex flex-col items-center gap-1">
                    <div className="text-sm font-semibold text-white">Comment</div>
                    <span className="text-white text-xs font-bold">
                      {formatCount(video.comments)}
                    </span>
                  </button>

                  {/* Share */}
                  <button className="flex flex-col items-center gap-1">
                    <div className="text-sm font-semibold text-white">Share</div>
                    <span className="text-white text-xs font-bold">
                      {formatCount(video.shares)}
                    </span>
                  </button>

                  {/* Sound */}
                  <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">
                    β—‰
                  </button>
                </div>

                {/* Bottom Info */}
                <div className="absolute bottom-4 left-4 right-20">
                  <div className="flex items-center gap-2 mb-2">
                    <span className="text-white font-bold">@{video.author.username}</span>
                    {video.author.isVerified && <span className="text-blue-400">√</span>}
                  </div>

                  <p className="text-white text-sm mb-2">{video.caption}</p>

                  <div className="flex items-center gap-2 bg-black/40 px-3 py-1.5 rounded-full max-w-xs">
                    <span className="text-white text-xs">Music</span>
                    <span className="text-white text-xs truncate">{video.sound.name}</span>
                  </div>
                </div>
              </>
            )}
          </div>
        ))}
      </div>

      {/* Navigation */}
      <div className="absolute right-4 top-1/2 transform -translate-y-1/2 flex flex-col gap-4">
        <button
          onClick={() => handleScroll('up')}
          disabled={currentIndex === 0}
          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"
        >
          β–²
        </button>
        <button
          onClick={() => handleScroll('down')}
          disabled={currentIndex === videos.length - 1}
          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"
        >
          β–Ό
        </button>
      </div>

      {/* Progress */}
      <div className="absolute top-4 right-4 text-white text-sm bg-black/50 px-3 py-1 rounded-full">
        {currentIndex + 1} / {videos.length}
      </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. 1. Main difference TikTok vs YouTube?

  2. 2. What is the For You Page?

These are 2 of 7 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    TikTok-style feed

  • Horizontal ordering

    Arrange the line declaring the current video index in the feed:

  • Click in order

    Build the call that plays the current video (click in order):

  • Code editor

    Video overlay component

  • Vertical ordering

    Order the actions on the right edge of the overlay (top to bottom):

  • Horizontal ordering

    Arrange the start of mapping video hashtags to JSX:

Useful articles