mirror of
https://github.com/hanzoai/shadcn.git
synced 2026-11-03 03:18:52 +00:00
Major fixes: - Fixed wagmi/Web3 SSR errors by lazy-loading config in Web3Provider - Added SidebarProvider to sidebar-demo components - Added 'use client' to 14 demo files with hooks/state/event handlers - Temporarily disabled force-static on blocks page Build progress: 0 → 258/1035 pages (25%) before hitting /blocks error Remaining issue: - /blocks page has function-passing error during static generation Package versions confirmed: - React 19.2.0 ✅ - Next.js 16.0.0 ✅ - Tailwind CSS 4.1.14 ✅
15 lines
23 KiB
JSON
15 lines
23 KiB
JSON
{
|
|
"name": "video-player",
|
|
"description": "Advanced video player component",
|
|
"dependencies": [
|
|
"video.js",
|
|
"videojs-react-enhanced"
|
|
],
|
|
"files": [
|
|
{
|
|
"name": "video-player.tsx",
|
|
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport {\n Captions,\n Maximize,\n Minimize,\n Pause,\n PictureInPicture,\n Play,\n RotateCcw,\n Settings,\n SkipBack,\n SkipForward,\n Volume2,\n VolumeX,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport { Button } from \"./button\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"./dropdown-menu\"\nimport { Slider } from \"./slider\"\n\n// Types\nexport interface VideoSource {\n src: string\n type: string\n quality?: string\n}\n\nexport interface SubtitleTrack {\n src: string\n label: string\n srcLang: string\n default?: boolean\n}\n\nexport interface VideoPlayerProps\n extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n | \"onPlay\"\n | \"onPause\"\n | \"onEnded\"\n | \"controls\"\n | \"onTimeUpdate\"\n | \"onVolumeChange\"\n > {\n sources: VideoSource[]\n poster?: string\n subtitles?: SubtitleTrack[]\n autoPlay?: boolean\n loop?: boolean\n muted?: boolean\n preload?: \"auto\" | \"metadata\" | \"none\"\n controls?: boolean\n disableKeyboard?: boolean\n disablePictureInPicture?: boolean\n onPlay?: () => void\n onPause?: () => void\n onEnded?: () => void\n onTimeUpdate?: (currentTime: number, duration: number) => void\n onVolumeChange?: (volume: number) => void\n onQualityChange?: (quality: string) => void\n onSpeedChange?: (speed: number) => void\n}\n\nconst videoPlayerVariants = cva(\n \"relative bg-black rounded-lg overflow-hidden group\",\n {\n variants: {\n size: {\n sm: \"aspect-video max-w-md\",\n md: \"aspect-video max-w-2xl\",\n lg: \"aspect-video max-w-4xl\",\n full: \"w-full aspect-video\",\n },\n },\n defaultVariants: {\n size: \"md\",\n },\n }\n)\n\nexport interface VideoPlayerVariantProps\n extends VariantProps<typeof videoPlayerVariants> {}\n\nconst VideoPlayer = React.forwardRef<\n HTMLDivElement,\n VideoPlayerProps & VideoPlayerVariantProps\n>(\n (\n {\n className,\n size,\n sources,\n poster,\n subtitles = [],\n autoPlay = false,\n loop = false,\n muted = false,\n preload = \"metadata\",\n controls = true,\n disableKeyboard = false,\n disablePictureInPicture = false,\n onPlay,\n onPause,\n onEnded,\n onTimeUpdate,\n onVolumeChange,\n onQualityChange,\n onSpeedChange,\n ...props\n },\n ref\n ) => {\n const videoRef = React.useRef<HTMLVideoElement>(null)\n const containerRef = React.useRef<HTMLDivElement>(null)\n const progressRef = React.useRef<HTMLDivElement>(null)\n const controlsTimeoutRef = React.useRef<NodeJS.Timeout | undefined>(\n undefined\n )\n\n // State\n const [isPlaying, setIsPlaying] = React.useState(false)\n const [currentTime, setCurrentTime] = React.useState(0)\n const [duration, setDuration] = React.useState(0)\n const [volume, setVolume] = React.useState(1)\n const [isMuted, setIsMuted] = React.useState(muted)\n const [isFullscreen, setIsFullscreen] = React.useState(false)\n const [showControls, setShowControls] = React.useState(true)\n const [playbackSpeed, setPlaybackSpeed] = React.useState(1)\n const [currentQuality, setCurrentQuality] = React.useState(\n sources.find((s) => s.quality)?.quality || \"auto\"\n )\n const [showSubtitles, setShowSubtitles] = React.useState(false)\n const [isBuffering, setIsBuffering] = React.useState(false)\n const [isPictureInPicture, setIsPictureInPicture] = React.useState(false)\n\n // Playback speeds\n const playbackSpeeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]\n\n // Quality options\n const qualityOptions = React.useMemo(() => {\n const qualities = sources\n .filter((s) => s.quality)\n .map((s) => s.quality!)\n .filter((quality, index, arr) => arr.indexOf(quality) === index)\n return [\"auto\", ...qualities]\n }, [sources])\n\n // Format time helper\n const formatTime = (time: number) => {\n const hours = Math.floor(time / 3600)\n const minutes = Math.floor((time % 3600) / 60)\n const seconds = Math.floor(time % 60)\n\n if (hours > 0) {\n return `${hours}:${minutes.toString().padStart(2, \"0\")}:${seconds.toString().padStart(2, \"0\")}`\n }\n return `${minutes}:${seconds.toString().padStart(2, \"0\")}`\n }\n\n // Toggle play/pause\n const togglePlayPause = React.useCallback(() => {\n if (!videoRef.current) return\n\n if (isPlaying) {\n videoRef.current.pause()\n } else {\n videoRef.current.play()\n }\n }, [isPlaying])\n\n // Seek to time\n const seekTo = React.useCallback((time: number) => {\n if (!videoRef.current) return\n videoRef.current.currentTime = time\n }, [])\n\n // Skip forward/backward\n const skip = React.useCallback(\n (seconds: number) => {\n if (!videoRef.current) return\n const newTime = Math.max(0, Math.min(duration, currentTime + seconds))\n seekTo(newTime)\n },\n [currentTime, duration, seekTo]\n )\n\n // Toggle mute\n const toggleMute = React.useCallback(() => {\n if (!videoRef.current) return\n\n if (isMuted) {\n videoRef.current.volume = volume\n setIsMuted(false)\n } else {\n videoRef.current.volume = 0\n setIsMuted(true)\n }\n }, [isMuted, volume])\n\n // Change volume\n const changeVolume = React.useCallback(\n (newVolume: number) => {\n if (!videoRef.current) return\n\n const clampedVolume = Math.max(0, Math.min(1, newVolume))\n videoRef.current.volume = clampedVolume\n setVolume(clampedVolume)\n setIsMuted(clampedVolume === 0)\n onVolumeChange?.(clampedVolume)\n },\n [onVolumeChange]\n )\n\n // Toggle fullscreen\n const toggleFullscreen = React.useCallback(async () => {\n if (!containerRef.current) return\n\n try {\n if (!document.fullscreenElement) {\n await containerRef.current.requestFullscreen()\n setIsFullscreen(true)\n } else {\n await document.exitFullscreen()\n setIsFullscreen(false)\n }\n } catch (error) {\n console.error(\"Fullscreen error:\", error)\n }\n }, [])\n\n // Toggle picture-in-picture\n const togglePictureInPicture = React.useCallback(async () => {\n if (!videoRef.current || disablePictureInPicture) return\n\n try {\n if (document.pictureInPictureElement) {\n await document.exitPictureInPicture()\n } else {\n await videoRef.current.requestPictureInPicture()\n }\n } catch (error) {\n console.error(\"Picture-in-picture error:\", error)\n }\n }, [disablePictureInPicture])\n\n // Change playback speed\n const changePlaybackSpeed = React.useCallback(\n (speed: number) => {\n if (!videoRef.current) return\n\n videoRef.current.playbackRate = speed\n setPlaybackSpeed(speed)\n onSpeedChange?.(speed)\n },\n [onSpeedChange]\n )\n\n // Change quality\n const changeQuality = React.useCallback(\n (quality: string) => {\n if (!videoRef.current) return\n\n const currentTimeBeforeChange = videoRef.current.currentTime\n const wasPlaying = !videoRef.current.paused\n\n if (quality === \"auto\") {\n // Use first source for auto\n videoRef.current.src = sources[0].src\n } else {\n const qualitySource = sources.find((s) => s.quality === quality)\n if (qualitySource) {\n videoRef.current.src = qualitySource.src\n }\n }\n\n videoRef.current.addEventListener(\n \"loadedmetadata\",\n () => {\n videoRef.current!.currentTime = currentTimeBeforeChange\n if (wasPlaying) {\n videoRef.current!.play()\n }\n },\n { once: true }\n )\n\n setCurrentQuality(quality)\n onQualityChange?.(quality)\n },\n [sources, onQualityChange]\n )\n\n // Hide controls after timeout\n const hideControlsAfterTimeout = React.useCallback(() => {\n if (controlsTimeoutRef.current) {\n clearTimeout(controlsTimeoutRef.current)\n }\n\n controlsTimeoutRef.current = setTimeout(() => {\n if (isPlaying) {\n setShowControls(false)\n }\n }, 3000)\n }, [isPlaying])\n\n // Show controls\n const showControlsTemporarily = React.useCallback(() => {\n setShowControls(true)\n hideControlsAfterTimeout()\n }, [hideControlsAfterTimeout])\n\n // Keyboard shortcuts\n React.useEffect(() => {\n if (disableKeyboard) return\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (!containerRef.current?.contains(document.activeElement)) return\n\n switch (e.code) {\n case \"Space\":\n e.preventDefault()\n togglePlayPause()\n break\n case \"KeyF\":\n e.preventDefault()\n toggleFullscreen()\n break\n case \"KeyM\":\n e.preventDefault()\n toggleMute()\n break\n case \"ArrowLeft\":\n e.preventDefault()\n skip(-10)\n break\n case \"ArrowRight\":\n e.preventDefault()\n skip(10)\n break\n case \"ArrowUp\":\n e.preventDefault()\n changeVolume(volume + 0.1)\n break\n case \"ArrowDown\":\n e.preventDefault()\n changeVolume(volume - 0.1)\n break\n case \"KeyC\":\n e.preventDefault()\n setShowSubtitles(!showSubtitles)\n break\n case \"KeyP\":\n e.preventDefault()\n togglePictureInPicture()\n break\n }\n\n showControlsTemporarily()\n }\n\n document.addEventListener(\"keydown\", handleKeyDown)\n return () => document.removeEventListener(\"keydown\", handleKeyDown)\n }, [\n disableKeyboard,\n togglePlayPause,\n toggleFullscreen,\n toggleMute,\n skip,\n changeVolume,\n volume,\n showSubtitles,\n togglePictureInPicture,\n showControlsTemporarily,\n ])\n\n // Video event handlers\n React.useEffect(() => {\n const video = videoRef.current\n if (!video) return\n\n const handlePlay = () => {\n setIsPlaying(true)\n setIsBuffering(false)\n onPlay?.()\n hideControlsAfterTimeout()\n }\n\n const handlePause = () => {\n setIsPlaying(false)\n setIsBuffering(false)\n onPause?.()\n setShowControls(true)\n }\n\n const handleTimeUpdate = () => {\n const currentTime = video.currentTime\n const duration = video.duration\n setCurrentTime(currentTime)\n setDuration(duration)\n onTimeUpdate?.(currentTime, duration)\n }\n\n const handleLoadedMetadata = () => {\n setDuration(video.duration)\n setIsBuffering(false)\n }\n\n const handleWaiting = () => {\n setIsBuffering(true)\n }\n\n const handleCanPlay = () => {\n setIsBuffering(false)\n }\n\n const handleEnded = () => {\n setIsPlaying(false)\n setShowControls(true)\n onEnded?.()\n }\n\n const handleVolumeChange = () => {\n setVolume(video.volume)\n setIsMuted(video.muted)\n }\n\n const handleEnterpictureinpicture = () => {\n setIsPictureInPicture(true)\n }\n\n const handleLeavepictureinpicture = () => {\n setIsPictureInPicture(false)\n }\n\n video.addEventListener(\"play\", handlePlay)\n video.addEventListener(\"pause\", handlePause)\n video.addEventListener(\"timeupdate\", handleTimeUpdate)\n video.addEventListener(\"loadedmetadata\", handleLoadedMetadata)\n video.addEventListener(\"waiting\", handleWaiting)\n video.addEventListener(\"canplay\", handleCanPlay)\n video.addEventListener(\"ended\", handleEnded)\n video.addEventListener(\"volumechange\", handleVolumeChange)\n video.addEventListener(\n \"enterpictureinpicture\",\n handleEnterpictureinpicture\n )\n video.addEventListener(\n \"leavepictureinpicture\",\n handleLeavepictureinpicture\n )\n\n return () => {\n video.removeEventListener(\"play\", handlePlay)\n video.removeEventListener(\"pause\", handlePause)\n video.removeEventListener(\"timeupdate\", handleTimeUpdate)\n video.removeEventListener(\"loadedmetadata\", handleLoadedMetadata)\n video.removeEventListener(\"waiting\", handleWaiting)\n video.removeEventListener(\"canplay\", handleCanPlay)\n video.removeEventListener(\"ended\", handleEnded)\n video.removeEventListener(\"volumechange\", handleVolumeChange)\n video.removeEventListener(\n \"enterpictureinpicture\",\n handleEnterpictureinpicture\n )\n video.removeEventListener(\n \"leavepictureinpicture\",\n handleLeavepictureinpicture\n )\n }\n }, [onPlay, onPause, onTimeUpdate, onEnded, hideControlsAfterTimeout])\n\n // Fullscreen change handler\n React.useEffect(() => {\n const handleFullscreenChange = () => {\n setIsFullscreen(!!document.fullscreenElement)\n }\n\n document.addEventListener(\"fullscreenchange\", handleFullscreenChange)\n return () =>\n document.removeEventListener(\"fullscreenchange\", handleFullscreenChange)\n }, [])\n\n // Mouse movement handler for controls\n React.useEffect(() => {\n const container = containerRef.current\n if (!container) return\n\n const handleMouseMove = () => {\n showControlsTemporarily()\n }\n\n const handleMouseLeave = () => {\n if (isPlaying && controlsTimeoutRef.current) {\n clearTimeout(controlsTimeoutRef.current)\n controlsTimeoutRef.current = setTimeout(() => {\n setShowControls(false)\n }, 1000)\n }\n }\n\n container.addEventListener(\"mousemove\", handleMouseMove)\n container.addEventListener(\"mouseleave\", handleMouseLeave)\n\n return () => {\n container.removeEventListener(\"mousemove\", handleMouseMove)\n container.removeEventListener(\"mouseleave\", handleMouseLeave)\n }\n }, [isPlaying, showControlsTemporarily])\n\n return (\n <div\n ref={containerRef}\n className={cn(videoPlayerVariants({ size, className }))}\n tabIndex={0}\n {...props}\n >\n <video\n ref={videoRef}\n className=\"w-full h-full object-contain\"\n poster={poster}\n autoPlay={autoPlay}\n loop={loop}\n muted={muted}\n preload={preload}\n playsInline\n onClick={togglePlayPause}\n >\n {sources.map((source, index) => (\n <source key={index} src={source.src} type={source.type} />\n ))}\n {subtitles.map((track, index) => (\n <track\n key={index}\n kind=\"subtitles\"\n src={track.src}\n srcLang={track.srcLang}\n label={track.label}\n default={track.default || index === 0}\n />\n ))}\n Your browser does not support the video tag.\n </video>\n\n {/* Loading indicator */}\n {isBuffering && (\n <div className=\"absolute inset-0 flex items-center justify-center bg-black/50\">\n <div className=\"animate-spin rounded-full h-8 w-8 border-b-2 border-white\"></div>\n </div>\n )}\n\n {/* Controls */}\n {controls && (\n <div\n className={cn(\n \"absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent transition-opacity duration-300\",\n showControls ? \"opacity-100\" : \"opacity-0\"\n )}\n >\n {/* Progress bar */}\n <div className=\"absolute bottom-16 left-4 right-4\">\n <div\n ref={progressRef}\n className=\"h-1 bg-white/30 rounded-full cursor-pointer group/progress\"\n onClick={(e) => {\n if (!progressRef.current) return\n const rect = progressRef.current.getBoundingClientRect()\n const percentage = (e.clientX - rect.left) / rect.width\n seekTo(percentage * duration)\n }}\n >\n <div\n className=\"h-full bg-primary rounded-full relative group-hover/progress:h-1.5 transition-all\"\n style={{ width: `${(currentTime / duration) * 100}%` }}\n >\n <div className=\"absolute -top-1 -right-1 w-3 h-3 bg-primary rounded-full opacity-0 group-hover/progress:opacity-100 transition-opacity\" />\n </div>\n </div>\n </div>\n\n {/* Control buttons */}\n <div className=\"absolute bottom-4 left-4 right-4 flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8 text-white hover:bg-white/20\"\n onClick={() => skip(-10)}\n >\n <RotateCcw className=\"h-4 w-4\" />\n </Button>\n\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-10 w-10 text-white hover:bg-white/20\"\n onClick={togglePlayPause}\n >\n {isPlaying ? (\n <Pause className=\"h-5 w-5\" />\n ) : (\n <Play className=\"h-5 w-5\" />\n )}\n </Button>\n\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8 text-white hover:bg-white/20\"\n onClick={() => skip(10)}\n >\n <SkipForward className=\"h-4 w-4\" />\n </Button>\n\n <div className=\"flex items-center gap-2 ml-4\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8 text-white hover:bg-white/20\"\n onClick={toggleMute}\n >\n {isMuted || volume === 0 ? (\n <VolumeX className=\"h-4 w-4\" />\n ) : (\n <Volume2 className=\"h-4 w-4\" />\n )}\n </Button>\n\n <div className=\"w-20\">\n <Slider\n value={[isMuted ? 0 : volume]}\n max={1}\n step={0.1}\n onValueChange={([value]) => changeVolume(value)}\n className=\"w-full\"\n />\n </div>\n </div>\n\n <div className=\"text-white text-sm ml-4\">\n {formatTime(currentTime)} / {formatTime(duration)}\n </div>\n </div>\n\n <div className=\"flex items-center gap-2\">\n {subtitles.length > 0 && (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className={cn(\n \"h-8 w-8 text-white hover:bg-white/20\",\n showSubtitles && \"bg-white/20\"\n )}\n onClick={() => setShowSubtitles(!showSubtitles)}\n >\n <Captions className=\"h-4 w-4\" />\n </Button>\n )}\n\n {/* Speed selector */}\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 px-2 text-white hover:bg-white/20\"\n >\n {playbackSpeed}x\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent>\n {playbackSpeeds.map((speed) => (\n <DropdownMenuItem\n key={speed}\n onClick={() => changePlaybackSpeed(speed)}\n className={cn(playbackSpeed === speed && \"bg-accent\")}\n >\n {speed}x\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n\n {/* Quality selector */}\n {qualityOptions.length > 1 && (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8 text-white hover:bg-white/20\"\n >\n <Settings className=\"h-4 w-4\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent>\n {qualityOptions.map((quality) => (\n <DropdownMenuItem\n key={quality}\n onClick={() => changeQuality(quality)}\n className={cn(\n currentQuality === quality && \"bg-accent\"\n )}\n >\n {quality}\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n\n {!disablePictureInPicture && (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className={cn(\n \"h-8 w-8 text-white hover:bg-white/20\",\n isPictureInPicture && \"bg-white/20\"\n )}\n onClick={togglePictureInPicture}\n >\n <PictureInPicture className=\"h-4 w-4\" />\n </Button>\n )}\n\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8 text-white hover:bg-white/20\"\n onClick={toggleFullscreen}\n >\n {isFullscreen ? (\n <Minimize className=\"h-4 w-4\" />\n ) : (\n <Maximize className=\"h-4 w-4\" />\n )}\n </Button>\n </div>\n </div>\n </div>\n )}\n </div>\n )\n }\n)\n\nVideoPlayer.displayName = \"VideoPlayer\"\n\nexport { VideoPlayer, videoPlayerVariants }\n"
|
|
}
|
|
],
|
|
"type": "components:ui"
|
|
} |