|
3 | 3 | import { useRouter } from 'next/navigation'; |
4 | 4 | import Link from 'next/link'; |
5 | 5 | import { Button } from '@/components/ui/button'; |
6 | | -import { ArrowLeft } from 'lucide-react'; |
| 6 | +import { ArrowLeft, Play, Pause, Volume2, VolumeX, Maximize } from 'lucide-react'; |
| 7 | +import { useState, useRef, useEffect } from 'react'; |
7 | 8 | import { |
8 | 9 | Accordion, |
9 | 10 | AccordionContent, |
10 | 11 | AccordionItem, |
11 | 12 | AccordionTrigger, |
12 | 13 | } from "@/components/ui/accordion" |
13 | 14 |
|
| 15 | +const MinimalistVideoPlayer = ({ src }: { src: string }) => { |
| 16 | + const videoRef = useRef<HTMLVideoElement>(null); |
| 17 | + const [isPlaying, setIsPlaying] = useState(false); |
| 18 | + const [isMuted, setIsMuted] = useState(false); |
| 19 | + const [progress, setProgress] = useState(0); |
| 20 | + const [duration, setDuration] = useState(0); |
| 21 | + const [currentTime, setCurrentTime] = useState(0); |
| 22 | + const [showControls, setShowControls] = useState(true); |
| 23 | + |
| 24 | + useEffect(() => { |
| 25 | + const video = videoRef.current; |
| 26 | + if (!video) return; |
| 27 | + |
| 28 | + const updateTime = () => { |
| 29 | + setCurrentTime(video.currentTime); |
| 30 | + setProgress((video.currentTime / video.duration) * 100); |
| 31 | + }; |
| 32 | + |
| 33 | + const updateDuration = () => { |
| 34 | + setDuration(video.duration); |
| 35 | + }; |
| 36 | + |
| 37 | + video.addEventListener('timeupdate', updateTime); |
| 38 | + video.addEventListener('loadedmetadata', updateDuration); |
| 39 | + |
| 40 | + return () => { |
| 41 | + video.removeEventListener('timeupdate', updateTime); |
| 42 | + video.removeEventListener('loadedmetadata', updateDuration); |
| 43 | + }; |
| 44 | + }, []); |
| 45 | + |
| 46 | + const togglePlay = () => { |
| 47 | + const video = videoRef.current; |
| 48 | + if (!video) return; |
| 49 | + |
| 50 | + if (isPlaying) { |
| 51 | + video.pause(); |
| 52 | + } else { |
| 53 | + video.play(); |
| 54 | + } |
| 55 | + setIsPlaying(!isPlaying); |
| 56 | + }; |
| 57 | + |
| 58 | + const toggleMute = () => { |
| 59 | + const video = videoRef.current; |
| 60 | + if (!video) return; |
| 61 | + |
| 62 | + video.muted = !isMuted; |
| 63 | + setIsMuted(!isMuted); |
| 64 | + }; |
| 65 | + |
| 66 | + const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => { |
| 67 | + const video = videoRef.current; |
| 68 | + if (!video) return; |
| 69 | + |
| 70 | + const rect = e.currentTarget.getBoundingClientRect(); |
| 71 | + const clickX = e.clientX - rect.left; |
| 72 | + const width = rect.width; |
| 73 | + const newTime = (clickX / width) * duration; |
| 74 | + |
| 75 | + video.currentTime = newTime; |
| 76 | + }; |
| 77 | + |
| 78 | + const toggleFullscreen = () => { |
| 79 | + const video = videoRef.current; |
| 80 | + if (!video) return; |
| 81 | + |
| 82 | + if (document.fullscreenElement) { |
| 83 | + document.exitFullscreen(); |
| 84 | + } else { |
| 85 | + video.requestFullscreen(); |
| 86 | + } |
| 87 | + }; |
| 88 | + |
| 89 | + const formatTime = (time: number) => { |
| 90 | + const minutes = Math.floor(time / 60); |
| 91 | + const seconds = Math.floor(time % 60); |
| 92 | + return `${minutes}:${seconds.toString().padStart(2, '0')}`; |
| 93 | + }; |
| 94 | + |
| 95 | + return ( |
| 96 | + <div |
| 97 | + className="relative w-full bg-black rounded-lg overflow-hidden group" |
| 98 | + onMouseEnter={() => setShowControls(true)} |
| 99 | + onMouseLeave={() => setShowControls(false)} |
| 100 | + > |
| 101 | + <video |
| 102 | + ref={videoRef} |
| 103 | + className="w-full h-auto" |
| 104 | + src={src} |
| 105 | + onClick={togglePlay} |
| 106 | + /> |
| 107 | + |
| 108 | + {/* Controls Overlay */} |
| 109 | + <div className={`absolute inset-0 bg-gradient-to-t from-black/50 to-transparent transition-opacity duration-300 ${showControls ? 'opacity-100' : 'opacity-0'}`}> |
| 110 | + {/* Play/Pause Button in Center */} |
| 111 | + <div className="absolute inset-0 flex items-center justify-center"> |
| 112 | + <button |
| 113 | + onClick={togglePlay} |
| 114 | + className="bg-black/50 hover:bg-black/70 text-white p-4 rounded-full transition-all duration-200 hover:scale-110" |
| 115 | + aria-label={isPlaying ? "Pause video" : "Play video"} |
| 116 | + title={isPlaying ? "Pause video" : "Play video"} |
| 117 | + > |
| 118 | + {isPlaying ? ( |
| 119 | + <Pause className="w-8 h-8" /> |
| 120 | + ) : ( |
| 121 | + <Play className="w-8 h-8 ml-1" /> |
| 122 | + )} |
| 123 | + </button> |
| 124 | + </div> |
| 125 | + |
| 126 | + {/* Bottom Controls */} |
| 127 | + <div className="absolute bottom-0 left-0 right-0 p-4"> |
| 128 | + {/* Progress Bar */} |
| 129 | + <div |
| 130 | + className="w-full h-1 bg-white/30 rounded-full cursor-pointer mb-3 hover:h-2 transition-all duration-200" |
| 131 | + onClick={handleProgressClick} |
| 132 | + role="progressbar" |
| 133 | + aria-label="Video progress" |
| 134 | + aria-valuenow={progress} |
| 135 | + aria-valuemin={0} |
| 136 | + aria-valuemax={100} |
| 137 | + title={`Video progress: ${Math.round(progress)}%`} |
| 138 | + > |
| 139 | + <div |
| 140 | + className="h-full bg-blue-500 rounded-full transition-all duration-200" |
| 141 | + style={{ width: `${progress}%` }} |
| 142 | + /> |
| 143 | + </div> |
| 144 | + |
| 145 | + {/* Control Buttons */} |
| 146 | + <div className="flex items-center justify-between"> |
| 147 | + <div className="flex items-center space-x-3"> |
| 148 | + <button |
| 149 | + onClick={togglePlay} |
| 150 | + className="text-white hover:text-blue-400 transition-colors duration-200" |
| 151 | + aria-label={isPlaying ? "Pause video" : "Play video"} |
| 152 | + title={isPlaying ? "Pause video" : "Play video"} |
| 153 | + > |
| 154 | + {isPlaying ? ( |
| 155 | + <Pause className="w-5 h-5" /> |
| 156 | + ) : ( |
| 157 | + <Play className="w-5 h-5" /> |
| 158 | + )} |
| 159 | + </button> |
| 160 | + |
| 161 | + <button |
| 162 | + onClick={toggleMute} |
| 163 | + className="text-white hover:text-blue-400 transition-colors duration-200" |
| 164 | + aria-label={isMuted ? "Unmute video" : "Mute video"} |
| 165 | + title={isMuted ? "Unmute video" : "Mute video"} |
| 166 | + > |
| 167 | + {isMuted ? ( |
| 168 | + <VolumeX className="w-5 h-5" /> |
| 169 | + ) : ( |
| 170 | + <Volume2 className="w-5 h-5" /> |
| 171 | + )} |
| 172 | + </button> |
| 173 | + |
| 174 | + <span className="text-white text-sm"> |
| 175 | + {formatTime(currentTime)} / {formatTime(duration)} |
| 176 | + </span> |
| 177 | + </div> |
| 178 | + |
| 179 | + <button |
| 180 | + onClick={toggleFullscreen} |
| 181 | + className="text-white hover:text-blue-400 transition-colors duration-200" |
| 182 | + aria-label="Toggle fullscreen" |
| 183 | + title="Toggle fullscreen" |
| 184 | + > |
| 185 | + <Maximize className="w-5 h-5" /> |
| 186 | + </button> |
| 187 | + </div> |
| 188 | + </div> |
| 189 | + </div> |
| 190 | + </div> |
| 191 | + ); |
| 192 | +}; |
| 193 | + |
14 | 194 | const DocsPage = () => { |
15 | 195 | const router = useRouter(); |
16 | 196 |
|
@@ -96,16 +276,8 @@ const DocsPage = () => { |
96 | 276 |
|
97 | 277 | <section id="demo" className="mb-8"> |
98 | 278 | <h2 className="text-2xl font-semibold mb-4">Demo</h2> |
99 | | - <div className="relative w-full overflow-hidden rounded-lg" style={{ paddingTop: '56.25%' }}> |
100 | | - <iframe |
101 | | - className="absolute top-0 left-0 w-full h-full" |
102 | | - src="https://www.youtube.com/embed/49qcrWgHT9M?si=2BnYxyBXXmh1EQd5" |
103 | | - title="YouTube video player" |
104 | | - frameBorder="0" |
105 | | - allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" |
106 | | - referrerPolicy="strict-origin-when-cross-origin" |
107 | | - allowFullScreen |
108 | | - ></iframe> |
| 279 | + <div className="w-full"> |
| 280 | + <MinimalistVideoPlayer src="/video/demo2.webm" /> |
109 | 281 | </div> |
110 | 282 | </section> |
111 | 283 |
|
|
0 commit comments