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