@@ -25,8 +25,10 @@ export default function VideoPlayer({
2525 const [ hasWatched , setHasWatched ] = useState ( false ) ;
2626 const [ currentTime , setCurrentTime ] = useState ( 0 ) ;
2727 const [ duration , setDuration ] = useState ( 0 ) ;
28+ const [ isBuffering , setIsBuffering ] = useState ( false ) ;
29+ const [ error , setError ] = useState < string | null > ( null ) ;
30+ const [ loadProgress , setLoadProgress ] = useState ( 0 ) ;
2831
29- // Use ref to track if we've already saved progress
3032 const progressSavedRef = useRef ( false ) ;
3133
3234 const saveVideoProgress = async ( ) => {
@@ -35,16 +37,20 @@ export default function VideoPlayer({
3537
3638 progressSavedRef . current = true ;
3739
38- await updateVideoProgress ( {
39- applicantId,
40- videoNumber,
41- watchedDuration : video . currentTime ,
42- totalDuration : video . duration ,
43- completed : true ,
44- } ) ;
45-
46- // Notify parent component to refresh progress state (to unlock Next button)
47- onVideoComplete ( ) ;
40+ try {
41+ await updateVideoProgress ( {
42+ applicantId,
43+ videoNumber,
44+ watchedDuration : video . currentTime ,
45+ totalDuration : video . duration ,
46+ completed : true ,
47+ } ) ;
48+ onVideoComplete ( ) ;
49+ } catch ( err ) {
50+ console . error ( "Failed to save progress:" , err ) ;
51+ // Reset so user can try again
52+ progressSavedRef . current = false ;
53+ }
4854 } ;
4955
5056 useEffect ( ( ) => {
@@ -56,41 +62,105 @@ export default function VideoPlayer({
5662 const percentWatched = ( video . currentTime / video . duration ) * 100 ;
5763 setProgress ( percentWatched ) ;
5864
59- // Mark as watched if they've seen 98% or more (to unlock Next button)
60- // But don't auto-advance - user must click Next manually
6165 if ( percentWatched >= 98 && ! progressSavedRef . current ) {
6266 setHasWatched ( true ) ;
63- // Save progress in background without triggering navigation
6467 saveVideoProgress ( ) ;
6568 }
6669 } ;
6770
6871 const handleLoadedMetadata = ( ) => {
6972 setDuration ( video . duration ) ;
73+ setError ( null ) ;
74+ } ;
75+
76+ const handlePlay = ( ) => {
77+ setIsPlaying ( true ) ;
78+ setIsBuffering ( false ) ;
7079 } ;
7180
72- const handlePlay = ( ) => setIsPlaying ( true ) ;
7381 const handlePause = ( ) => setIsPlaying ( false ) ;
7482
83+ // NEW: Buffering detection
84+ const handleWaiting = ( ) => {
85+ setIsBuffering ( true ) ;
86+ } ;
87+
88+ const handleCanPlay = ( ) => {
89+ setIsBuffering ( false ) ;
90+ } ;
91+
92+ const handlePlaying = ( ) => {
93+ setIsBuffering ( false ) ;
94+ } ;
95+
96+ // NEW: Error handling
97+ const handleError = ( ) => {
98+ setError ( "Failed to load video. Please refresh the page or try again later." ) ;
99+ setIsBuffering ( false ) ;
100+ } ;
101+
102+ // NEW: Track loading progress
103+ const handleProgress = ( ) => {
104+ if ( video . buffered . length > 0 ) {
105+ const bufferedEnd = video . buffered . end ( video . buffered . length - 1 ) ;
106+ const loadPercent = ( bufferedEnd / video . duration ) * 100 ;
107+ setLoadProgress ( loadPercent ) ;
108+ }
109+ } ;
110+
111+ // NEW: Stalled detection (when download has stopped)
112+ const handleStalled = ( ) => {
113+ console . warn ( "Video download stalled" ) ;
114+ setIsBuffering ( true ) ;
115+ } ;
116+
117+ const handleSuspend = ( ) => {
118+ // Browser stopped fetching media data
119+ console . log ( "Video loading suspended" ) ;
120+ } ;
121+
75122 video . addEventListener ( "timeupdate" , handleTimeUpdate ) ;
76123 video . addEventListener ( "loadedmetadata" , handleLoadedMetadata ) ;
77124 video . addEventListener ( "play" , handlePlay ) ;
78125 video . addEventListener ( "pause" , handlePause ) ;
126+ video . addEventListener ( "waiting" , handleWaiting ) ;
127+ video . addEventListener ( "canplay" , handleCanPlay ) ;
128+ video . addEventListener ( "playing" , handlePlaying ) ;
129+ video . addEventListener ( "error" , handleError ) ;
130+ video . addEventListener ( "progress" , handleProgress ) ;
131+ video . addEventListener ( "stalled" , handleStalled ) ;
132+ video . addEventListener ( "suspend" , handleSuspend ) ;
79133
80134 return ( ) => {
81135 video . removeEventListener ( "timeupdate" , handleTimeUpdate ) ;
82136 video . removeEventListener ( "loadedmetadata" , handleLoadedMetadata ) ;
83137 video . removeEventListener ( "play" , handlePlay ) ;
84138 video . removeEventListener ( "pause" , handlePause ) ;
139+ video . removeEventListener ( "waiting" , handleWaiting ) ;
140+ video . removeEventListener ( "canplay" , handleCanPlay ) ;
141+ video . removeEventListener ( "playing" , handlePlaying ) ;
142+ video . removeEventListener ( "error" , handleError ) ;
143+ video . removeEventListener ( "progress" , handleProgress ) ;
144+ video . removeEventListener ( "stalled" , handleStalled ) ;
145+ video . removeEventListener ( "suspend" , handleSuspend ) ;
85146 } ;
86- } , [ ] ) ; // Empty dependency array - only set up listeners once
147+ } , [ ] ) ;
87148
88149 const formatTime = ( seconds : number ) => {
150+ if ( ! isFinite ( seconds ) ) return "0:00" ;
89151 const mins = Math . floor ( seconds / 60 ) ;
90152 const secs = Math . floor ( seconds % 60 ) ;
91153 return `${ mins } :${ secs . toString ( ) . padStart ( 2 , "0" ) } ` ;
92154 } ;
93155
156+ const handleRetry = ( ) => {
157+ setError ( null ) ;
158+ setIsBuffering ( false ) ;
159+ if ( videoRef . current ) {
160+ videoRef . current . load ( ) ;
161+ }
162+ } ;
163+
94164 if ( ! canWatch ) {
95165 return (
96166 < div className = "flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-700 bg-gray-900/20 p-12 text-center" >
@@ -123,11 +193,56 @@ export default function VideoPlayer({
123193 className = "h-full w-full"
124194 controls
125195 controlsList = "nodownload"
126- onContextMenu = { ( e ) => e . preventDefault ( ) } // Disable right-click
196+ onContextMenu = { ( e ) => e . preventDefault ( ) }
197+ preload = "auto" // NEW: Start loading immediately
198+ playsInline // NEW: Better mobile support
127199 >
128200 < source src = { videoUrl } type = "video/mp4" />
129201 Your browser does not support the video tag.
130202 </ video >
203+
204+ { /* NEW: Buffering overlay */ }
205+ { isBuffering && ! error && (
206+ < div className = "absolute inset-0 flex items-center justify-center bg-black/50" >
207+ < div className = "flex flex-col items-center gap-3" >
208+ < div className = "h-12 w-12 animate-spin rounded-full border-4 border-gray-600 border-t-customTeal" > </ div >
209+ < p className = "text-sm text-white" > Loading video...</ p >
210+ { loadProgress > 0 && loadProgress < 100 && (
211+ < p className = "text-xs text-gray-400" >
212+ { Math . round ( loadProgress ) } % buffered
213+ </ p >
214+ ) }
215+ </ div >
216+ </ div >
217+ ) }
218+
219+ { /* NEW: Error overlay */ }
220+ { error && (
221+ < div className = "absolute inset-0 flex items-center justify-center bg-black/80" >
222+ < div className = "flex flex-col items-center gap-4 px-6 text-center" >
223+ < svg
224+ className = "h-12 w-12 text-red-500"
225+ fill = "none"
226+ viewBox = "0 0 24 24"
227+ stroke = "currentColor"
228+ >
229+ < path
230+ strokeLinecap = "round"
231+ strokeLinejoin = "round"
232+ strokeWidth = { 2 }
233+ d = "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
234+ />
235+ </ svg >
236+ < div >
237+ < h3 className = "text-lg font-semibold text-white" > Video Error</ h3 >
238+ < p className = "mt-2 text-sm text-gray-300" > { error } </ p >
239+ </ div >
240+ < Button onClick = { handleRetry } className = "mt-2" >
241+ Retry
242+ </ Button >
243+ </ div >
244+ </ div >
245+ ) }
131246 </ div >
132247
133248 { /* Progress Bar */ }
@@ -138,22 +253,42 @@ export default function VideoPlayer({
138253 { formatTime ( currentTime ) } / { formatTime ( duration ) }
139254 </ span >
140255 </ div >
256+
257+ { /* Watch Progress */ }
141258 < div className = "h-2 w-full overflow-hidden rounded-full bg-gray-800" >
142259 < div
143260 className = "h-full bg-gradient-to-r from-customTeal via-customBlue-100 to-customViolet-100 transition-all duration-300"
144261 style = { { width : `${ progress } %` } }
145262 />
146263 </ div >
264+
265+ { /* NEW: Buffer Progress (shows how much has loaded) */ }
266+ { loadProgress > 0 && loadProgress < 100 && (
267+ < div className = "space-y-1" >
268+ < span className = "text-xs text-gray-500" >
269+ Buffered: { Math . round ( loadProgress ) } %
270+ </ span >
271+ < div className = "h-1 w-full overflow-hidden rounded-full bg-gray-800" >
272+ < div
273+ className = "h-full bg-gray-600 transition-all duration-300"
274+ style = { { width : `${ loadProgress } %` } }
275+ />
276+ </ div >
277+ </ div >
278+ ) }
147279 </ div >
148280
149- { /* Status Indicator */ }
150- { hasWatched && (
281+ { /* Status Indicators */ }
282+ { isBuffering && ! error && (
283+ < div className = "flex items-center justify-center gap-2 rounded-lg bg-yellow-900/20 border border-yellow-500 p-3 text-yellow-400" >
284+ < div className = "h-4 w-4 animate-spin rounded-full border-2 border-yellow-400 border-t-transparent" > </ div >
285+ < span className = "text-sm font-medium" > Buffering video...</ span >
286+ </ div >
287+ ) }
288+
289+ { hasWatched && ! error && (
151290 < div className = "flex items-center justify-center gap-2 rounded-lg bg-green-900/20 border border-green-500 p-3 text-green-400" >
152- < svg
153- className = "h-5 w-5"
154- fill = "currentColor"
155- viewBox = "0 0 20 20"
156- >
291+ < svg className = "h-5 w-5" fill = "currentColor" viewBox = "0 0 20 20" >
157292 < path
158293 fillRule = "evenodd"
159294 d = "M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
0 commit comments