Skip to content

Commit d309163

Browse files
authored
a
1 parent dee9e99 commit d309163

File tree

2 files changed

+475
-10
lines changed

2 files changed

+475
-10
lines changed

_layouts/post.html

Lines changed: 341 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,365 @@
11
---
2-
layout: default
2+
layout: default
33
---
44

5+
<!--
6+
================================================================================
7+
POST LAYOUT WITH INTEGRATED VIDEO PLAYER
8+
================================================================================
9+
This layout is designed for course lessons. It checks for a 'videoid' in the
10+
front matter. If found, it displays a custom, lazy-loading YouTube player
11+
before the main content.
12+
-->
13+
514
{% include adsense.html type="leaderboard" %}
615

716
<article class="l-post">
8-
<div class="post-heading">
9-
<!-- Breadcrumbs can be made dynamic later with a plugin, for now we can simulate it -->
17+
18+
<!-- ============================================= -->
19+
<!-- 1. POST HEADING -->
20+
<!-- ============================================= -->
21+
<div class="post-heading">
1022
<ol class="breadcrumb-list post-heading__breadcrumb">
1123
<li><a href="{{ '/' | relative_url }}">Home</a></li>
12-
<li><a href="#">{{ page.category }}</a></li>
24+
<li><a href="{{ '/categories/' | relative_url }}#{{ page.category | slugify }}">{{ page.category }}</a></li>
1325
<li><span aria-current="page">{{ page.title }}</span></li>
1426
</ol>
1527
<h1 class="post-heading__title">{{ page.title }}</h1>
28+
{% if page.description %}
29+
<p class="post-heading__description">{{ page.description }}</p>
30+
{% endif %}
1631
</div>
17-
<!-- ============================================= -->
18-
<!-- 2. TABLE OF CONTENTS (TOC) - Placeholder -->
32+
33+
<!-- ============================================= -->
34+
<!-- 2. LEARNING PATH SIDEBAR -->
1935
<!-- ============================================= -->
2036
<div class="l-post__toc toc" aria-labelledby="toc-title">
21-
{% include toc.html html=content h_min=2 h_max=3 %}
37+
<div class="learning-path">
38+
<h2 class="toc__title" id="toc-title">Learning Path</h2>
39+
{% assign sorted_lessons = site.posts | sort: "order" %}
40+
{% assign grouped_by_section = sorted_lessons | group_by: "category" %}
41+
{% for section in grouped_by_section %}
42+
<div class="learning-path__section">
43+
<h3 class="learning-path__category">{{ section.name }}</h3>
44+
<ul class="learning-path__list">
45+
{% for lesson in section.items %}
46+
<li class="learning-path__lesson">
47+
<a href="{{ lesson.url }}" {% if page.url == lesson.url %}class="is-active" aria-current="page"{% endif %}>
48+
<span>{{ lesson.order }} - {{ lesson.title }}</span>
49+
</a>
50+
</li>
51+
{% endfor %}
52+
</ul>
53+
</div>
54+
{% endfor %}
55+
</div>
2256
</div>
2357

58+
<!-- ============================================= -->
59+
<!-- 3. MAIN CONTENT AREA -->
60+
<!-- ============================================= -->
2461
<div class="l-post__content post-content">
25-
{% include adsense.html type="article" %}
62+
63+
<!-- VIDEO PLAYER (only renders if 'videoid' exists) -->
64+
{% if page.type == "video" and page.videoid %}
65+
<div class="video-player"
66+
id="video-player-container"
67+
data-videoid="{{ page.videoid }}"
68+
data-start="{{ page.start_time | default: 0 }}"
69+
data-end="{{ page.end_time }}">
70+
71+
<!-- 1. The Custom Thumbnail (Visible by default) -->
72+
<div class="video-player__poster" id="video-player-poster">
73+
<div class="video-player__poster-shape-1"></div>
74+
<div class="video-player__poster-shape-2"></div>
75+
76+
<div class="video-player__poster-content">
77+
<p class="video-player__poster-headline">{{ page.category | upcase }}</p>
78+
<h2 class="video-player__poster-title">{{ page.title }}</h2>
79+
</div>
80+
81+
<div class="video-player__play-button">
82+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8,5.14V19.14L19,12.14L8,5.14Z" /></svg>
83+
</div>
84+
<div class="video-player__completed-badge">
85+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M10 17L5 12L6.41 10.59L10 14.17L17.59 6.58L19 8L10 17Z" /></svg>
86+
<span>Lesson Complete</span>
87+
</div>
88+
</div>
89+
90+
<!-- 2. The YouTube Iframe will be created here by JS -->
91+
<div id="video-player-iframe-target"></div>
92+
</div>
93+
94+
<div class="ad ad-under-video">
95+
{% include adsense.html format="horizontal" %}
96+
</div>
97+
{% endif %}
2698

2799
{{ content }}
28100
{% include post-comments.html %}
29-
30101
</div>
31102

32103
</article>
104+
33105
{% include adsense.html type="multiplex" %}
34-
{% include post-paginate.html %}
106+
{% include post-paginate.html %}
107+
108+
<!-- ===================================================================== -->
109+
<!-- EMBEDDED CSS & JAVASCRIPT FOR THE VIDEO PLAYER -->
110+
<!-- ===================================================================== -->
111+
<style>
112+
/* --- Video Player Styles --- */
113+
.video-player {
114+
position: relative;
115+
padding-bottom: 56.25%; /* 16:9 aspect ratio */
116+
height: 0;
117+
overflow: hidden;
118+
background-color: #000;
119+
border-radius: var(--spruce-border-radius-lg);
120+
margin-block-end: 2rem;
121+
border: 1px solid var(--spruce-base-color-border);
122+
}
123+
.video-player iframe {
124+
position: absolute;
125+
top: 0;
126+
left: 0;
127+
width: 100%;
128+
height: 100%;
129+
border: 0;
130+
}
131+
.ad-under-video { margin-block: 2rem 2.5rem; }
132+
133+
/* --- Custom Thumbnail Design (using SpruceCSS variables) --- */
134+
.video-player__poster {
135+
position: absolute;
136+
top: 0;
137+
left: 0;
138+
width: 100%;
139+
height: 100%;
140+
z-index: 10;
141+
cursor: pointer;
142+
background-color: var(--spruce-base-color-code-background);
143+
display: flex;
144+
justify-content: center;
145+
align-items: center;
146+
font-family: var(--spruce-font-family-heading);
147+
transition: opacity 0.3s ease-in-out;
148+
overflow: hidden;
149+
}
150+
.video-player__poster.is-hidden { display: none; }
151+
152+
/* Decorative Shapes */
153+
.video-player__poster-shape-1, .video-player__poster-shape-2 {
154+
position: absolute;
155+
border-radius: 50%;
156+
mix-blend-mode: multiply;
157+
opacity: 0.6;
158+
}
159+
.video-player__poster-shape-1 {
160+
width: 45%;
161+
height: 70%;
162+
background: var(--spruce-base-color-secondary);
163+
top: -20%;
164+
left: -10%;
165+
}
166+
.video-player__poster-shape-2 {
167+
width: 40%;
168+
height: 60%;
169+
background: var(--spruce-base-color-primary);
170+
bottom: -25%;
171+
right: -15%;
172+
}
173+
[data-theme-mode=dark] .video-player__poster-shape-1,
174+
[data-theme-mode=dark] .video-player__poster-shape-2 {
175+
mix-blend-mode: screen;
176+
opacity: 0.5;
177+
}
178+
179+
/* Text Content */
180+
.video-player__poster-content {
181+
position: relative;
182+
z-index: 2;
183+
text-align: center;
184+
color: var(--spruce-base-color-heading);
185+
padding: 1rem;
186+
}
187+
.video-player__poster-headline {
188+
font-size: clamp(0.8rem, 1.5vw, 1rem);
189+
font-weight: 700;
190+
letter-spacing: 3px;
191+
opacity: 0.8;
192+
color: var(--spruce-base-color-primary);
193+
}
194+
.video-player__poster-title {
195+
font-size: clamp(1.5rem, 3.5vw, 2.5rem);
196+
font-weight: 700;
197+
line-height: 1.2;
198+
margin-top: 0.5rem;
199+
max-width: 25ch;
200+
}
201+
202+
/* Play Button */
203+
.video-player__play-button {
204+
position: absolute;
205+
z-index: 3;
206+
top: 50%;
207+
left: 50%;
208+
transform: translate(-50%, -50%);
209+
color: var(--spruce-btn-color-primary-foreground);
210+
background-color: var(--spruce-btn-color-primary-background);
211+
width: clamp(50px, 10vw, 64px);
212+
height: clamp(50px, 10vw, 64px);
213+
border-radius: 50%;
214+
display: flex;
215+
justify-content: center;
216+
align-items: center;
217+
transition: all 0.2s ease;
218+
box-shadow: var(--spruce-box-shadow);
219+
}
220+
.video-player__play-button svg {
221+
width: 50%; height: 50%;
222+
}
223+
.video-player__poster:hover .video-player__play-button {
224+
transform: translate(-50%, -50%) scale(1.1);
225+
background-color: var(--spruce-btn-color-primary-background-hover);
226+
}
227+
228+
/* Completed Badge */
229+
.video-player__completed-badge {
230+
position: absolute;
231+
z-index: 12;
232+
display: none;
233+
align-items: center;
234+
justify-content: center;
235+
transform: translate(-50%, -50%);
236+
top: 50%; left: 50%;
237+
background: hsla(0, 0%, 10%, 0.8);
238+
color: #fff;
239+
padding: 1rem 2rem;
240+
border-radius: 50px;
241+
font-size: clamp(1rem, 2vw, 1.25rem);
242+
font-weight: 600;
243+
backdrop-filter: blur(5px);
244+
}
245+
.video-player__poster.is-completed { cursor: default; }
246+
.video-player__poster.is-completed .video-player__play-button,
247+
.video-player__poster.is-completed .video-player__poster-content { display: none; }
248+
.video-player__poster.is-completed .video-player__completed-badge { display: flex; }
249+
.video-player__completed-badge svg {
250+
width: 1.5em;
251+
height: 1.5em;
252+
margin-right: 0.75rem;
253+
color: var(--spruce-alert-color-success);
254+
}
255+
256+
/* --- Learning Path Sidebar Styles --- */
257+
.learning-path__section { margin-block-end: 2rem; }
258+
.learning-path__category {
259+
font-size: 1rem;
260+
font-weight: 700;
261+
color: var(--spruce-base-color-heading);
262+
margin-block-end: 0.75rem;
263+
}
264+
.learning-path__list { list-style: none; margin: 0; padding: 0; }
265+
.learning-path__lesson a {
266+
display: block;
267+
padding: 0.5rem 1rem;
268+
border-radius: var(--spruce-border-radius-sm);
269+
text-decoration: none;
270+
color: var(--spruce-base-color-text);
271+
line-height: var(--spruce-line-height-sm);
272+
}
273+
.learning-path__lesson a:hover {
274+
background-color: var(--spruce-base-color-border);
275+
color: var(--spruce-base-color-heading);
276+
}
277+
.learning-path__lesson a.is-active {
278+
background-color: var(--spruce-base-color-primary);
279+
color: var(--spruce-btn-color-primary-foreground);
280+
font-weight: 600;
281+
}
282+
</style>
283+
284+
<script>
285+
// This function is required by the YouTube Iframe API and will be called
286+
// automatically when the API script has loaded.
287+
function onYouTubeIframeAPIReady() {
288+
// Find any player on the page that has been clicked and marked for initialization
289+
const playerContainer = document.querySelector('.video-player[data-init-player="true"]');
290+
if (!playerContainer) return;
291+
292+
// Remove the attribute to prevent re-initializing if the API is loaded multiple times
293+
playerContainer.removeAttribute('data-init-player');
294+
295+
const videoId = playerContainer.dataset.videoid;
296+
const startTime = parseInt(playerContainer.dataset.start, 10);
297+
const endTime = playerContainer.dataset.end ? parseInt(playerContainer.dataset.end, 10) : null;
298+
let player;
299+
let progressChecker;
300+
301+
function cleanupPlayer() {
302+
clearInterval(progressChecker);
303+
if (player && typeof player.destroy === 'function') {
304+
player.destroy();
305+
}
306+
const poster = playerContainer.querySelector('.video-player__poster');
307+
poster.classList.remove('is-hidden');
308+
poster.classList.add('is-completed');
309+
}
310+
311+
player = new YT.Player('video-player-iframe-target', {
312+
videoId: videoId,
313+
playerVars: {
314+
'autoplay': 1,
315+
'controls': 1,
316+
'rel': 0,
317+
'start': startTime,
318+
'showinfo': 0,
319+
'modestbranding': 1
320+
},
321+
events: {
322+
'onStateChange': (event) => {
323+
if (event.data === YT.PlayerState.PLAYING && endTime) {
324+
progressChecker = setInterval(() => {
325+
if (player.getCurrentTime() >= endTime) {
326+
cleanupPlayer();
327+
}
328+
}, 1000);
329+
} else if (event.data === YT.PlayerState.ENDED) {
330+
cleanupPlayer();
331+
} else {
332+
clearInterval(progressChecker);
333+
}
334+
}
335+
}
336+
});
337+
}
338+
339+
// Set up the click listener for the poster image
340+
document.addEventListener('DOMContentLoaded', () => {
341+
const playerContainer = document.getElementById('video-player-container');
342+
const poster = document.getElementById('video-player-poster');
343+
344+
if (!playerContainer || !poster) return; // Exit if no video player on this page
345+
346+
poster.addEventListener('click', () => {
347+
// Do nothing if the video has already been played and marked as complete
348+
if (poster.classList.contains('is-completed')) return;
349+
350+
poster.classList.add('is-hidden');
351+
playerContainer.setAttribute('data-init-player', 'true');
352+
353+
// Check if the YouTube IFrame API script is already loaded
354+
if (typeof(YT) == 'undefined' || typeof(YT.Player) == 'undefined') {
355+
const tag = document.createElement('script');
356+
tag.src = "https://www.youtube.com/iframe_api";
357+
const firstScriptTag = document.getElementsByTagName('script')[0];
358+
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
359+
} else {
360+
// If the API is already loaded, just create the player
361+
onYouTubeIframeAPIReady();
362+
}
363+
});
364+
});
365+
</script>

0 commit comments

Comments
 (0)