Player.tsx raw

   1  import { cn } from '@/lib/utils'
   2  import { ExternalLink, Play } from 'lucide-react'
   3  import { memo, useState } from 'react'
   4  
   5  interface PlayerProps {
   6    videoId: string
   7    isShort: boolean
   8    className?: string
   9  }
  10  
  11  /**
  12   * Privacy-preserving YouTube thumbnail display.
  13   *
  14   * Does NOT load any YouTube scripts or iframes. Shows a static thumbnail
  15   * from YouTube's image CDN and opens the video in a new tab when clicked.
  16   *
  17   * This eliminates all tracking that YouTube's embedded player performs:
  18   * - No /youtubei/v1/log_event telemetry
  19   * - No cookies set
  20   * - No browser fingerprinting
  21   * - No behavioral tracking on scroll/visibility
  22   */
  23  const Player = memo(({ videoId, isShort, className }: PlayerProps) => {
  24    const [imgError, setImgError] = useState(false)
  25  
  26    // YouTube thumbnail URLs - try maxres first, fallback to hqdefault
  27    // maxresdefault.jpg may not exist for all videos
  28    const thumbnailUrl = imgError
  29      ? `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`
  30      : `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`
  31  
  32    // Construct the direct YouTube URL
  33    const youtubeUrl = isShort
  34      ? `https://www.youtube.com/shorts/${videoId}`
  35      : `https://www.youtube.com/watch?v=${videoId}`
  36  
  37    return (
  38      <a
  39        href={youtubeUrl}
  40        target="_blank"
  41        rel="noopener noreferrer"
  42        onClick={(e) => e.stopPropagation()}
  43        className={cn(
  44          'block rounded-xl border overflow-hidden cursor-pointer relative group',
  45          isShort ? 'aspect-[9/16] max-h-[80vh] sm:max-h-[60vh]' : 'aspect-video max-h-[60vh]',
  46          className
  47        )}
  48      >
  49        {/* Thumbnail image */}
  50        <img
  51          src={thumbnailUrl}
  52          alt="YouTube video thumbnail"
  53          className="w-full h-full object-cover"
  54          loading="lazy"
  55          onError={() => !imgError && setImgError(true)}
  56        />
  57  
  58        {/* Play button overlay */}
  59        <div className="absolute inset-0 flex items-center justify-center bg-black/20 group-hover:bg-black/40 transition-colors">
  60          <div className="bg-red-600 rounded-full p-4 group-hover:scale-110 transition-transform shadow-lg">
  61            <Play className="size-8 text-white fill-white" />
  62          </div>
  63        </div>
  64  
  65        {/* External link indicator */}
  66        <div className="absolute top-2 right-2 bg-black/60 rounded-md px-2 py-1 flex items-center gap-1 text-white text-xs opacity-0 group-hover:opacity-100 transition-opacity">
  67          <ExternalLink className="size-3" />
  68          <span>YouTube</span>
  69        </div>
  70      </a>
  71    )
  72  })
  73  
  74  Player.displayName = 'YoutubePlayer'
  75  
  76  export default Player
  77