Skip to main content

Overview

Jefftube organizes videos into playlists based on the camera location or type of footage. Playlists provide a curated viewing experience, allowing you to watch related videos sequentially.

Playlist Organization

Videos are automatically grouped into playlists based on their playlist field in the database:
export const videos = pgTable("videos", {
  id: varchar("id", { length: 50 }).primaryKey(),
  title: varchar("title", { length: 255 }),
  playlist: varchar("playlist", { length: 100 }), // e.g., "elevator-cam", "lobby-cam"
  // ... other fields
});

Example Playlists

Elevator Cam

Videos recorded from elevator security cameras

Lobby Cam

Footage from lobby and entrance areas

Other Collections

Additional categorized video groups
Some videos may not belong to any playlist and appear as standalone content on the channel page.

Viewing Playlists

From the Channel Page

1

Navigate to Playlists Tab

On the main channel page, click the Playlists tab (next to the Videos tab).
2

Browse Playlist Cards

Each playlist is displayed as a card showing:
  • Thumbnail from the first video
  • Playlist name (formatted with title case)
  • Video count
  • Total duration of all videos
3

Click to Play

Click any playlist card or the “Play all” overlay to start watching from the first video.

Playlist Card Interface

<Link to={`/playlist/${playlist.id}/${firstVideo.id}`}>
  <div className="relative aspect-video rounded-xl overflow-hidden">
    <img src={getThumbnailUrl(firstVideo)} alt={playlist.name} />
    
    {/* Playlist overlay */}
    <div className="absolute right-0 w-[40%] bg-black/80">
      <span>{playlist.videos.length}</span>
      <PlayIcon />
    </div>
    
    {/* Hover overlay */}
    <div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100">
      <PlayIcon />
      <span>Play all</span>
    </div>
  </div>
  
  <div className="mt-3">
    <h3>{playlist.name}</h3>
    <p>{playlist.videos.length} videos • {formatDuration(totalDuration)} total</p>
  </div>
</Link>

Playlist Video Page

When you click a playlist or a video within a playlist, you’re taken to the playlist video page at /playlist/:playlistId/:videoId.

Playlist Sidebar

The playlist video page features a dedicated sidebar that shows:

Playlist Header

  • Playlist name (e.g., “Elevator Cam”)
  • Channel name: “Jeffery Epstein”
  • Current position (e.g., “5/23”)
  • “Play all” button
  • Total video count and duration

Video List

Scrollable list of all videos in the playlist with:
  • Sequential numbering (or play icon for current video)
  • Thumbnail preview
  • Video title
  • Duration badge
  • Channel name

Playlist Sidebar Layout

<aside className="w-full lg:w-[400px] border rounded-xl">
  {/* Header */}
  <div className="bg-secondary p-4">
    <h2>{playlistName}</h2>
    <p>Jeffery Epstein • {currentIndex + 1}/{videos.length}</p>
    
    <Link to={`/playlist/${playlistId}/${videos[0].id}`}>
      <button className="bg-primary rounded-full">
        <PlayIcon />
        Play all
      </button>
    </Link>
    
    <p className="text-xs">
      {videos.length} videos • {formatDuration(totalDuration)} total
    </p>
  </div>
  
  {/* Video list */}
  <div className="max-h-[600px] overflow-y-auto">
    {videos.map((video, index) => (
      <PlaylistSidebarCard 
        video={video}
        index={index}
        isActive={video.id === currentVideoId}
      />
    ))}
  </div>
</aside>

Current Video Indicator

The currently playing video is highlighted in the playlist:
  • Background color changes to tertiary
  • Play icon (▶) replaces the number
  • Title remains in primary text color
{isActive ? (
  <PlayIcon />
) : (
  <span>{index + 1}</span>
)}

Sequential Navigation

1

Click Next Video

Click any video card in the playlist sidebar to jump to that video.
2

URL Updates

The URL changes to /playlist/:playlistId/:newVideoId without page reload.
3

Video Loads

The video player updates to show the new video, and the sidebar highlights the new current position.

Play All Feature

Click the Play all button in the playlist header to:
  1. Navigate to the first video in the playlist
  2. Start playback immediately
  3. Allow sequential watching through the entire collection
<Link
  to={`/playlist/${playlistId}/${videos[0].id}`}
  className="flex items-center gap-2 px-4 py-2 bg-primary rounded-full"
>
  <PlayIcon />
  Play all
</Link>

Playlist Name Formatting

Playlist IDs use kebab-case (e.g., elevator-cam), which are automatically formatted to title case for display:
function formatPlaylistName(id: string): string {
  return id
    .split("-")
    .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
    .join(" ");
}

// Examples:
// "elevator-cam" → "Elevator Cam"
// "lobby-cam" → "Lobby Cam"
// "outdoor-footage" → "Outdoor Footage"
Using kebab-case IDs keeps URLs clean and URL-safe, while the formatting function provides user-friendly display names. This separation allows for easy changes to display names without breaking URLs.

Playlist Metadata

Each playlist automatically calculates:

Video Count

const playlistVideos = videos.filter((v) => v.playlist === playlistId);
const videoCount = playlistVideos.length;

Total Duration

const totalDuration = playlist.videos.reduce((acc, v) => acc + v.length, 0);
const formattedDuration = formatDuration(totalDuration);
// Example: "1:23:45" (1 hour, 23 minutes, 45 seconds)
Duration calculations happen in real-time based on the videos currently in the playlist, so the total always reflects the current state.

Playlist Video Cards

Each video in the playlist sidebar displays:
<Link to={`/playlist/${playlistId}/${video.id}`}>
  <div className="flex gap-2 p-2 rounded-lg">
    {/* Number or play icon */}
    <div className="w-6">
      {isActive ? <PlayIcon /> : <span>{index + 1}</span>}
    </div>
    
    {/* Thumbnail */}
    <div className="relative w-24 aspect-video rounded-lg">
      <img src={getThumbnailUrl(video)} alt={video.title} />
      <div className="absolute bottom-1 right-1 bg-overlay text-white">
        {formatDuration(video.length)}
      </div>
    </div>
    
    {/* Info */}
    <div className="flex-1 min-w-0">
      <h3 className="text-sm line-clamp-2">{video.title}</h3>
      <p className="text-xs text-secondary">Jeffery Epstein</p>
    </div>
    
    {/* More options (on hover) */}
    <button className="opacity-0 group-hover:opacity-100">
      <MoreVertIcon />
    </button>
  </div>
</Link>

Responsive Behavior

Mobile View

On mobile devices (< 1024px):
  • Playlist sidebar appears below the video player
  • Full width layout
  • Scrollable video list maintains 600px max height

Desktop View

On larger screens (≥ 1024px):
  • Playlist sidebar fixed at 400px width
  • Positioned to the right of the video player
  • Video player flexes to fill remaining space
<div className="flex flex-col lg:flex-row gap-6">
  {/* Main content */}
  <div className="flex-1 min-w-0">
    <VideoPlayer />
    <VideoInfo />
    <CommentSection />
  </div>
  
  {/* Playlist Sidebar - mobile: full width below, desktop: 400px right */}
  <PlaylistSidebar className="w-full lg:w-[400px]" />
</div>

Comments in Playlist Context

When watching from a playlist:
  • Comments are video-specific, not playlist-specific
  • Each video maintains its own comment thread
  • Switching videos in the playlist loads that video’s comments
Comments always reference the individual video, regardless of whether you accessed it from a playlist or directly. This ensures consistency across different entry points.

Playlist Generation

Playlists are dynamically generated from the video database:
// Group videos by playlist
const playlists = useMemo(() => {
  const playlistMap = new Map<string, Video[]>();
  
  videos.forEach((video) => {
    if (video.playlist) {
      const existing = playlistMap.get(video.playlist) || [];
      existing.push(video);
      playlistMap.set(video.playlist, existing);
    }
  });
  
  return Array.from(playlistMap.entries()).map(([id, vids]) => ({
    id,
    name: formatPlaylistName(id),
    videos: vids,
  }));
}, [videos]);
This approach means playlists automatically update when videos are added or removed from the database. No manual playlist management is required.

Build docs developers (and LLMs) love