Skip to content

Commit 970beea

Browse files
authored
Initial Commit
Uploaded base files and resources for Placeholder Demo.
1 parent 2962999 commit 970beea

7 files changed

Lines changed: 944 additions & 0 deletions

File tree

app.js

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// Demo Hub — hash-based router & views
2+
const app = document.getElementById("app");
3+
4+
function esc(s) {
5+
return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
6+
}
7+
8+
// Accepts a bare ID, a full watch URL, a youtu.be link, or a playlist URL.
9+
// Returns { videoId, playlistId } — either may be null.
10+
function parseYouTube(input) {
11+
if (!input) return { videoId: null, playlistId: null };
12+
const raw = String(input).trim();
13+
// Bare 11-char video id (no slashes / query)
14+
if (/^[\w-]{11}$/.test(raw)) return { videoId: raw, playlistId: null };
15+
let videoId = null,
16+
playlistId = null;
17+
try {
18+
const u = new URL(raw);
19+
videoId = u.searchParams.get("v");
20+
playlistId = u.searchParams.get("list");
21+
if (!videoId && u.hostname.includes("youtu.be")) {
22+
videoId = u.pathname.slice(1).split("/")[0] || null;
23+
}
24+
if (!videoId && u.pathname.includes("/embed/")) {
25+
videoId = u.pathname.split("/embed/")[1].split("/")[0] || null;
26+
}
27+
} catch (e) {
28+
// Not a URL — fall through with nulls
29+
}
30+
return { videoId: videoId || null, playlistId: playlistId || null };
31+
}
32+
33+
// Build a privacy-friendly embed URL that supports single videos AND (unlisted) playlists.
34+
// Pass { jsapi: true } to enable the IFrame API (needed for auto-advance).
35+
function embedUrl(video, opts = {}) {
36+
const parsed = parseYouTube(video.youtubeId);
37+
const playlistId = video.playlistId || parsed.playlistId;
38+
const base = { rel: "0", modestbranding: "1", playsinline: "1" };
39+
if (opts.jsapi) {
40+
base.enablejsapi = "1";
41+
base.origin = location.origin;
42+
}
43+
const params = new URLSearchParams(base);
44+
let path;
45+
if (parsed.videoId) {
46+
path = parsed.videoId;
47+
if (playlistId) params.set("list", playlistId); // play this video within the (unlisted) playlist
48+
} else if (playlistId) {
49+
// Playlist-only: use the playlist embed endpoint
50+
return `https://www.youtube-nocookie.com/embed/videoseries?${params.toString()}&list=${encodeURIComponent(playlistId)}`;
51+
} else {
52+
path = "";
53+
}
54+
return `https://www.youtube-nocookie.com/embed/${path}?${params.toString()}`;
55+
}
56+
57+
// Load the YouTube IFrame API once and auto-advance to `nextHash` when the video ends.
58+
let ytApiPromise = null;
59+
function loadYouTubeAPI() {
60+
if (window.YT && window.YT.Player) return Promise.resolve();
61+
if (ytApiPromise) return ytApiPromise;
62+
ytApiPromise = new Promise((resolve) => {
63+
const tag = document.createElement("script");
64+
tag.src = "https://www.youtube.com/iframe_api";
65+
window.onYouTubeIframeAPIReady = () => resolve();
66+
document.head.appendChild(tag);
67+
});
68+
return ytApiPromise;
69+
}
70+
71+
function setupAutoAdvance(nextHash) {
72+
const iframe = document.getElementById("yt-player");
73+
if (!iframe) return;
74+
loadYouTubeAPI().then(() => {
75+
// Re-check: user may have navigated away before the API loaded.
76+
if (document.getElementById("yt-player") !== iframe) return;
77+
new window.YT.Player("yt-player", {
78+
events: {
79+
onStateChange: (e) => {
80+
if (e.data === window.YT.PlayerState.ENDED && nextHash) {
81+
location.hash = nextHash;
82+
}
83+
},
84+
},
85+
});
86+
});
87+
}
88+
89+
function thumbUrl(video) {
90+
const { videoId } = parseYouTube(video.youtubeId);
91+
return videoId
92+
? `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`
93+
: "https://img.youtube.com/vi/videoseries/hqdefault.jpg";
94+
}
95+
96+
function render() {
97+
const hash = location.hash.slice(1); // e.g. /product/crm/video/crm-1
98+
const parts = hash.split("/").filter(Boolean);
99+
if (parts[0] === "product" && parts[2] === "video") {
100+
renderVideo(parts[1], parts[3]);
101+
} else if (parts[0] === "product") {
102+
renderDashboard(parts[1]);
103+
} else {
104+
renderHome();
105+
}
106+
window.scrollTo(0, 0);
107+
}
108+
109+
function renderHome() {
110+
const cards = PRODUCTS.map(
111+
(p) => `
112+
<div class="glass card" onclick="location.hash='#/product/${p.id}'">
113+
<div class="icon">${p.iconSvg ? `<img src="${p.iconSvg}" alt="${esc(p.name)}" style="width:32px;height:32px;object-fit:contain;" />` : p.icon}</div>
114+
<h3>${esc(p.name)}</h3>
115+
<p>${esc(p.tagline)}</p>
116+
<div class="count">${p.videos.length} video${p.videos.length === 1 ? "" : "s"}</div>
117+
</div>`
118+
).join("");
119+
app.innerHTML = `
120+
<header class="hero fade">
121+
<h1>Your BrowserStack Demo Hub</h1><h2> Explore, Learn, Launch</h2>
122+
<p>Explore interactive demos of BrowserStack's powerful testing features, all in one place. Whether you're new or looking to deepen your product knowledge, this hub helps you quickly understand, experience, and onboard with BrowserStack.
123+
124+
</p>
125+
</header>
126+
<div class="grid fade">${cards}</div>`;
127+
}
128+
129+
function renderDashboard(pid) {
130+
const p = PRODUCTS.find((x) => x.id === pid);
131+
if (!p) return renderHome();
132+
const cards = p.videos.map(
133+
(v) => `
134+
<div class="glass card vthumb" onclick="location.hash='#/product/${p.id}/video/${v.id}'">
135+
<div class="thumbwrap">
136+
<img class="thumbimg" src="${thumbUrl(v)}" alt="${esc(v.title)}" onerror="this.classList.add('noimg')" />
137+
<div class="play"><span>&#9654;</span></div>
138+
${v.duration ? `<span class="dur">${esc(v.duration)}</span>` : ""}
139+
</div>
140+
<div class="meta">
141+
<h3>${esc(v.title)}</h3>
142+
<p>${esc(v.description).slice(0, 90)}${v.description.length > 90 ? "…" : ""}</p>
143+
</div>
144+
</div>`
145+
).join("");
146+
app.innerHTML = `
147+
<div class="crumbs fade"><a onclick="location.hash='#/'">BrowserStack Demo Hub</a><span>›</span><span>${esc(p.name)}</span></div>
148+
<header class="hero fade" style="text-align:left;margin-bottom:32px;">
149+
<h1 style="font-size:2.4rem;">${p.icon} ${esc(p.name)}</h1>
150+
<h5>${esc(p.tagline)}</h5>
151+
</header>
152+
<div class="grid fade">${cards}</div>`;
153+
}
154+
155+
function renderVideo(pid, vid) {
156+
const p = PRODUCTS.find((x) => x.id === pid);
157+
const idx = p ? p.videos.findIndex((x) => x.id === vid) : -1;
158+
const v = idx >= 0 ? p.videos[idx] : null;
159+
if (!v) return renderHome();
160+
const docs = v.docs.map((d) => `<li><a href="${esc(d.url)}" target="_blank" rel="noopener">📄 ${esc(d.label)}</a></li>`).join("");
161+
const links = v.links.map((l) => `<li><a href="${esc(l.url)}" target="_blank" rel="noopener">🔗 ${esc(l.label)}</a></li>`).join("");
162+
const playlist = p.videos
163+
.map(
164+
(item, i) => `
165+
<li class="pl-item ${item.id === v.id ? "active" : ""}" onclick="location.hash='#/product/${p.id}/video/${item.id}'">
166+
<span class="pl-index">${i + 1}</span>
167+
<img class="pl-thumb" src="${thumbUrl(item)}" alt="" onerror="this.classList.add('noimg')" />
168+
<span class="pl-meta">
169+
<span class="pl-title">${esc(item.title)}</span>
170+
<span class="pl-sub">
171+
${item.duration ? `<span class="pl-dur">${esc(item.duration)}</span>` : ""}
172+
${item.id === v.id ? '<span class="pl-now">Now playing</span>' : ""}
173+
</span>
174+
</span>
175+
</li>`
176+
)
177+
.join("");
178+
app.innerHTML = `
179+
<div class="crumbs fade">
180+
<a onclick="location.hash='#/'">BrowserStack Demo Hub</a><span>›</span>
181+
<a onclick="location.hash='#/product/${p.id}'">${esc(p.name)}</a><span>›</span>
182+
<span>${esc(v.title)}</span>
183+
</div>
184+
<div class="detail fade">
185+
<div class="glass player-wrap">
186+
<iframe id="yt-player" src="${embedUrl(v, { jsapi: true })}" title="${esc(v.title)}" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
187+
</div>
188+
<div class="glass panel desc-panel">
189+
<h2>${esc(v.title)}</h2>
190+
<p>${esc(v.description)}</p>
191+
</div>
192+
<div class="side">
193+
<div class="glass panel pl-panel">
194+
<h4>▶ Playlist &middot; ${p.videos.length} videos</h4>
195+
<ul class="playlist">${playlist}</ul>
196+
</div>
197+
<div class="glass panel">
198+
<h4>📚 Documentation</h4>
199+
<ul class="linklist">${docs || "<li><p>None yet.</p></li>"}</ul>
200+
</div>
201+
<div class="glass panel">
202+
<h4>🔗 Relevant Links</h4>
203+
<ul class="linklist">${links || "<li><p>None yet.</p></li>"}</ul>
204+
</div>
205+
</div>
206+
</div>`;
207+
// Auto-advance to the next video when this one ends.
208+
const next = p.videos[idx + 1];
209+
setupAutoAdvance(next ? `#/product/${p.id}/video/${next.id}` : null);
210+
// Keep the active playlist item in view.
211+
const active = document.querySelector(".pl-item.active");
212+
if (active) active.scrollIntoView({ block: "nearest" });
213+
}
214+
215+
window.addEventListener("hashchange", render);
216+
render();

browserstack-icon.svg

Lines changed: 1 addition & 0 deletions
Loading

0 commit comments

Comments
 (0)