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-
145{% include adsense.html type="leaderboard" %}
156
167< article class ="l-post ">
17-
18- <!-- ============================================= -->
19- <!-- 1. POST HEADING -->
20- <!-- ============================================= -->
21- < div class ="post-heading ">
8+ < div class ="post-heading ">
9+ <!-- Breadcrumbs can be made dynamic later with a plugin, for now we can simulate it -->
2210 < ol class ="breadcrumb-list post-heading__breadcrumb ">
2311 < li > < a href ="{{ '/' | relative_url }} "> Home</ a > </ li >
24- < li > < a href ="{{ '/categories/' | relative_url }}#{{ page.category | slugify }} "> {{ page.category }}</ a > </ li >
12+ < li > < a href ="# "> {{ page.category }}</ a > </ li >
2513 < li > < span aria-current ="page "> {{ page.title }}</ span > </ li >
2614 </ ol >
2715 < h1 class ="post-heading__title "> {{ page.title }}</ h1 >
28- {% if page.description %}
29- < p class ="post-heading__description "> {{ page.description }}</ p >
30- {% endif %}
3116 </ div >
32-
33- <!-- ============================================= -->
34- <!-- 2. LEARNING PATH SIDEBAR -->
17+ <!-- ============================================= -->
18+ <!-- 2. TABLE OF CONTENTS (TOC) - Placeholder -->
3519 <!-- ============================================= -->
3620 < div class ="l-post__toc toc " aria-labelledby ="toc-title ">
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 >
21+ {% include toc.html html=content h_min=2 h_max=3 %}
5622 </ div >
5723
58- <!-- ============================================= -->
59- <!-- 3. MAIN CONTENT AREA -->
60- <!-- ============================================= -->
6124 < div class ="l-post__content post-content ">
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 %}
25+ {% include adsense.html type="article" %}
9826
9927 {{ content }}
10028 {% include post-comments.html %}
29+
10130 </ div >
10231
10332</ article >
104-
10533{% include adsense.html type="multiplex" %}
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 >
34+ {% include post-paginate.html %}
0 commit comments