Skip to content

Commit 22af8e2

Browse files
committed
fix: walkthrough embeds
1 parent 868f370 commit 22af8e2

File tree

1 file changed

+14
-301
lines changed

1 file changed

+14
-301
lines changed

components/walkthroughs/WatchWalkthroughsPage.tsx

Lines changed: 14 additions & 301 deletions
Original file line numberDiff line numberDiff line change
@@ -1,303 +1,28 @@
11
import { Header } from "@/components/Header";
22
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
3-
import { ArrowRight } from "lucide-react";
43
import { Quote } from "@/components/Quote";
54
import Link from "next/link";
65
import { useRouter } from "next/router";
76
import { HomeSection } from "../home/components/HomeSection";
8-
import { useEffect, useRef, useState, useCallback } from "react";
9-
import { Button } from "@/components/ui/button";
107
import { 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-
209
interface 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

Comments
 (0)