11import { Header } from "@/components/Header" ;
22import { Tabs , TabsContent , TabsList , TabsTrigger } from "@/components/ui/tabs" ;
3- import { ArrowRight } from "lucide-react" ;
43import { Quote } from "@/components/Quote" ;
54import Link from "next/link" ;
65import { useRouter } from "next/router" ;
76import { HomeSection } from "../home/components/HomeSection" ;
8- import { useEffect , useRef , useState , useCallback } from "react" ;
9- import { Button } from "@/components/ui/button" ;
107import { WALKTHROUGH_TABS } from "./constants" ;
118
12- // Declare YouTube IFrame API types
13- declare global {
14- interface Window {
15- YT : any ;
16- onYouTubeIframeAPIReady : ( ) => void ;
17- }
18- }
19-
209interface VideoPlayerProps {
2110 videoId : string ;
2211 title : string ;
23- onVideoEnd ?: ( ) => void ;
24- hasNextVideo : boolean ;
25- nextVideoTitle ?: string ;
26- onNextVideo ?: ( ) => void ;
2712}
2813
29- function VideoPlayer ( {
30- videoId,
31- title,
32- onVideoEnd,
33- hasNextVideo,
34- nextVideoTitle,
35- onNextVideo,
36- } : VideoPlayerProps ) {
37- const playerRef = useRef < any > ( null ) ;
38- const containerRef = useRef < HTMLDivElement > ( null ) ;
39- const [ showOverlay , setShowOverlay ] = useState ( false ) ;
40- const progressCheckIntervalRef = useRef < NodeJS . Timeout | null > ( null ) ;
41- const isCheckingProgressRef = useRef ( false ) ;
42- const [ isApiReady , setIsApiReady ] = useState ( false ) ;
43-
44- const checkVideoProgress = useCallback ( ( ) => {
45- if ( ! playerRef . current || ! hasNextVideo ) return ;
46-
47- if ( isCheckingProgressRef . current ) return ;
48-
49- isCheckingProgressRef . current = true ;
50-
51- // Clear any existing progress check interval
52- if ( progressCheckIntervalRef . current ) {
53- clearInterval ( progressCheckIntervalRef . current ) ;
54- }
55-
56- progressCheckIntervalRef . current = setInterval ( ( ) => {
57- if ( ! playerRef . current ) {
58- if ( progressCheckIntervalRef . current ) {
59- clearInterval ( progressCheckIntervalRef . current ) ;
60- progressCheckIntervalRef . current = null ;
61- }
62- isCheckingProgressRef . current = false ;
63- return ;
64- }
65-
66- try {
67- const currentTime = playerRef . current . getCurrentTime ( ) ;
68- const duration = playerRef . current . getDuration ( ) ;
69- const timeRemaining = duration - currentTime ;
70-
71- // Show overlay when 10 seconds remaining
72- if ( timeRemaining <= 10 && timeRemaining > 0 ) {
73- setShowOverlay ( true ) ;
74- }
75-
76- // Stop checking if video is paused or ended
77- const state = playerRef . current . getPlayerState ( ) ;
78- if ( state !== 1 ) {
79- // Not playing
80- if ( progressCheckIntervalRef . current ) {
81- clearInterval ( progressCheckIntervalRef . current ) ;
82- progressCheckIntervalRef . current = null ;
83- }
84- isCheckingProgressRef . current = false ;
85- }
86- } catch ( e ) {
87- // Player methods failed, likely destroyed - clear interval
88- if ( progressCheckIntervalRef . current ) {
89- clearInterval ( progressCheckIntervalRef . current ) ;
90- progressCheckIntervalRef . current = null ;
91- }
92- isCheckingProgressRef . current = false ;
93- }
94- } , 500 ) ;
95- } , [ hasNextVideo ] ) ;
96-
97- const handleVideoEnd = useCallback ( ( ) => {
98- if ( ! hasNextVideo ) {
99- onVideoEnd ?.( ) ;
100- return ;
101- }
102-
103- // Show overlay without countdown
104- setShowOverlay ( true ) ;
105- } , [ hasNextVideo , onVideoEnd ] ) ;
106-
107- const onPlayerStateChange = useCallback (
108- ( event : any ) => {
109- // Playing
110- if ( event . data === 1 ) {
111- checkVideoProgress ( ) ;
112- }
113- // Ended
114- if ( event . data === 0 ) {
115- handleVideoEnd ( ) ;
116- }
117- } ,
118- [ checkVideoProgress , handleVideoEnd ]
119- ) ;
120-
121- // Use a ref to avoid reinitializing the player when callbacks change
122- const onPlayerStateChangeRef = useRef ( onPlayerStateChange ) ;
123- useEffect ( ( ) => {
124- onPlayerStateChangeRef . current = onPlayerStateChange ;
125- } , [ onPlayerStateChange ] ) ;
126-
127- const initPlayer = useCallback ( ( ) => {
128- if ( ! containerRef . current || ! window . YT || ! window . YT . Player ) return ;
129-
130- // Destroy existing player if it exists
131- if ( playerRef . current ) {
132- try {
133- playerRef . current . destroy ( ) ;
134- } catch ( e ) {
135- // Ignore errors during destruction
136- }
137- }
138-
139- // Clear the container to ensure clean state
140- if ( containerRef . current ) {
141- containerRef . current . innerHTML = "" ;
142- }
143-
144- try {
145- playerRef . current = new window . YT . Player ( containerRef . current , {
146- host : "https://www.youtube-nocookie.com" ,
147- videoId : videoId ,
148- playerVars : {
149- modestbranding : 1 ,
150- rel : 0 ,
151- } ,
152- events : {
153- onStateChange : ( event : any ) => onPlayerStateChangeRef . current ( event ) ,
154- onReady : ( event : any ) => {
155- // Add cookieyes attribute to the iframe element
156- try {
157- const iframe = event . target . getIframe ( ) ;
158- if ( iframe ) {
159- iframe . setAttribute ( "data-cookieyes" , "necessary" ) ;
160- }
161- } catch ( e ) {
162- console . error ( "Error setting cookieyes attribute:" , e ) ;
163- }
164- } ,
165- } ,
166- } ) ;
167- } catch ( e ) {
168- console . error ( "Error initializing YouTube player:" , e ) ;
169- }
170- } , [ videoId ] ) ;
171-
172- // Load YouTube IFrame API
173- useEffect ( ( ) => {
174- if ( typeof window === "undefined" ) return ;
175-
176- // Check if API is already loaded
177- if ( window . YT && window . YT . Player ) {
178- setIsApiReady ( true ) ;
179- return ;
180- }
181-
182- // Load the API if not already loaded
183- if ( ! document . getElementById ( "youtube-iframe-api" ) ) {
184- const tag = document . createElement ( "script" ) ;
185- tag . id = "youtube-iframe-api" ;
186- tag . src = "https://www.youtube.com/iframe_api" ;
187- // Mark as necessary for CookieYes to allow loading
188- tag . setAttribute ( "data-cookieyes" , "necessary" ) ;
189- const firstScriptTag = document . getElementsByTagName ( "script" ) [ 0 ] ;
190- firstScriptTag . parentNode ?. insertBefore ( tag , firstScriptTag ) ;
191-
192- window . onYouTubeIframeAPIReady = ( ) => {
193- setIsApiReady ( true ) ;
194- } ;
195- } else {
196- // Script exists but API might not be ready yet
197- const checkApi = setInterval ( ( ) => {
198- if ( window . YT && window . YT . Player ) {
199- setIsApiReady ( true ) ;
200- clearInterval ( checkApi ) ;
201- }
202- } , 100 ) ;
203-
204- return ( ) => clearInterval ( checkApi ) ;
205- }
206- } , [ ] ) ;
207-
208- // Initialize player when API is ready and videoId changes
209- useEffect ( ( ) => {
210- if ( isApiReady && videoId ) {
211- initPlayer ( ) ;
212- }
213-
214- return ( ) => {
215- if ( playerRef . current ) {
216- try {
217- playerRef . current . destroy ( ) ;
218- } catch ( e ) {
219- // Ignore errors during cleanup
220- }
221- }
222- } ;
223- } , [ isApiReady , videoId , initPlayer ] ) ;
224-
225- const handleNextClick = ( ) => {
226- setShowOverlay ( false ) ;
227- onNextVideo ?.( ) ;
228- } ;
229-
230- // Reset overlay state when videoId changes
231- useEffect ( ( ) => {
232- setShowOverlay ( false ) ;
233- if ( progressCheckIntervalRef . current ) {
234- clearInterval ( progressCheckIntervalRef . current ) ;
235- progressCheckIntervalRef . current = null ;
236- }
237- isCheckingProgressRef . current = false ;
238- } , [ videoId ] ) ;
239-
14+ function VideoPlayer ( { videoId, title } : VideoPlayerProps ) {
24015 return (
241- < div className = "relative w-full aspect-[16/9] rounded border overflow-hidden" >
242- < div
243- ref = { containerRef }
244- className = "absolute inset-0 w-full h-full z-10 pointer-events-none [&>*]:pointer-events-auto"
245- title = { title }
246- />
247-
248- < div className = "absolute inset-0 flex items-center justify-center bg-muted/50 backdrop-blur-sm z-0" >
249- < div className = "max-w-md mx-4 p-6 rounded-lg border bg-card shadow-lg text-center" >
250- < p className = "text-sm text-muted-foreground mb-4" >
251- This video is hosted on YouTube. Cookies are required to display the
252- video player.
253- </ p >
254- < div className = "flex gap-2 flex-col justify-center items-center" >
255- < Button className = "cky-banner-element" variant = "secondary" >
256- Enable Cookies
257- </ Button >
258- < Button asChild variant = "ghost" >
259- < a
260- href = { `https://www.youtube.com/watch?v=${ videoId } ` }
261- target = "_blank"
262- rel = "noopener noreferrer"
263- >
264- Open on YouTube
265- </ a >
266- </ Button >
267- </ div >
268- </ div >
269- </ div >
270-
271- { showOverlay && hasNextVideo && (
272- < div
273- className = "absolute top-0 left-0 right-0 bg-background/95 backdrop-blur-md z-20 border-b shadow-lg"
274- role = "alert"
275- aria-live = "polite"
276- >
277- < div className = "flex items-center justify-between gap-4 px-4 py-3" >
278- < div className = "flex items-center gap-3 flex-1 min-w-0" >
279- < div className = "text-sm font-medium whitespace-nowrap text-muted-foreground" >
280- Watch next:
281- </ div >
282- < div className = "text-sm font-medium text-foreground truncate" >
283- { nextVideoTitle }
284- </ div >
285- </ div >
286-
287- < div className = "flex items-center gap-2 flex-shrink-0" >
288- < Button
289- onClick = { handleNextClick }
290- size = "sm"
291- className = "whitespace-nowrap gap-1.5"
292- >
293- Play Next
294- < ArrowRight className = "size-4" />
295- </ Button >
296- </ div >
297- </ div >
298- </ div >
299- ) }
300- </ div >
16+ < iframe
17+ width = "100%"
18+ className = "aspect-[16/9] rounded mt-3"
19+ src = { `https://www.youtube-nocookie.com/embed/${ videoId } ` }
20+ title = { title }
21+ frameBorder = "0"
22+ allow = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
23+ referrerPolicy = "strict-origin-when-cross-origin"
24+ allowFullScreen
25+ />
30126 ) ;
30227}
30328
@@ -325,7 +50,7 @@ export function WatchWalkthroughsPage() {
32550 } ;
32651
32752 return (
328- < HomeSection className = "px-0" >
53+ < HomeSection className = "px-0 max-w-7xl " >
32954 < Header
33055 title = "Walkthroughs"
33156 description = "End-to-end walkthroughs of all Langfuse platform features"
@@ -350,17 +75,12 @@ export function WatchWalkthroughsPage() {
35075 ) ) }
35176 </ TabsList >
35277
353- { WALKTHROUGH_TABS . map ( ( tab , index ) => {
354- const tabHasNextVideo = index < WALKTHROUGH_TABS . length - 1 ;
355- const tabNextTab = tabHasNextVideo
356- ? WALKTHROUGH_TABS [ index + 1 ]
357- : null ;
358-
78+ { WALKTHROUGH_TABS . map ( ( tab ) => {
35979 return (
36080 < TabsContent
36181 key = { tab . id }
36282 value = { tab . id }
363- className = "mt-2 p-4 border rounded bg-card"
83+ className = "mt-2 p-4 border rounded bg-card max-w-2xl mx-auto "
36484 >
36585 < div className = "mb-6" >
36686 < h3 className = "text-xl font-semibold mb-2" > { tab . title } </ h3 >
@@ -376,13 +96,6 @@ export function WatchWalkthroughsPage() {
37696 < VideoPlayer
37797 videoId = { tab . videoId }
37898 title = { `Langfuse ${ tab . label . toLowerCase ( ) } video` }
379- hasNextVideo = { tabHasNextVideo }
380- nextVideoTitle = { tabNextTab ? tabNextTab . title : undefined }
381- onNextVideo = { ( ) => {
382- if ( tabHasNextVideo && tabNextTab ) {
383- handleTabChange ( tabNextTab . id ) ;
384- }
385- } }
38699 />
387100 < div className = "mt-4" >
388101 < div className = "text-sm font-medium mb-1" > Learn more:</ div >
0 commit comments