@@ -7,11 +7,14 @@ class BlogLoader {
77 constructor ( ) {
88 this . articles = [ ] ;
99 this . filteredArticles = [ ] ;
10+ this . featuredArticles = [ ] ;
1011 this . articlesContainer = null ;
12+ this . featuredCarousel = null ;
1113 this . searchInput = null ;
1214 this . sortSelect = null ;
1315 this . currentSearchTerm = '' ;
1416 this . currentSortBy = 'date-desc' ;
17+ this . carouselScrollPosition = 0 ;
1518 }
1619
1720 /**
@@ -20,6 +23,7 @@ class BlogLoader {
2023 async init ( ) {
2124 try {
2225 this . articlesContainer = document . querySelector ( '.articles-grid' ) ;
26+ this . featuredCarousel = document . getElementById ( 'featured-carousel' ) ;
2327 this . searchInput = document . getElementById ( 'blog-search' ) ;
2428 this . sortSelect = document . getElementById ( 'sort-by' ) ;
2529
@@ -34,9 +38,15 @@ class BlogLoader {
3438 // Load articles from JSON
3539 await this . loadArticles ( ) ;
3640
41+ // Render featured posts
42+ this . renderFeaturedPosts ( ) ;
43+
3744 // Setup event listeners
3845 this . setupEventListeners ( ) ;
3946
47+ // Setup carousel navigation
48+ this . setupCarouselNavigation ( ) ;
49+
4050 // Render articles
4151 this . renderArticles ( ) ;
4252
@@ -217,7 +227,10 @@ class BlogLoader {
217227 this . articles = data . articles . sort ( ( a , b ) => a . order - b . order ) ;
218228 this . filteredArticles = [ ...this . articles ] ;
219229
220- console . log ( `Loaded ${ this . articles . length } articles` ) ;
230+ // Extract featured articles
231+ this . featuredArticles = this . articles . filter ( article => article . featured === true ) ;
232+
233+ console . log ( `Loaded ${ this . articles . length } articles (${ this . featuredArticles . length } featured)` ) ;
221234
222235 } catch ( error ) {
223236 console . error ( 'Error loading articles:' , error ) ;
@@ -403,6 +416,163 @@ class BlogLoader {
403416 div . textContent = text ;
404417 return div . innerHTML ;
405418 }
419+
420+ /**
421+ * Render featured posts carousel
422+ */
423+ renderFeaturedPosts ( ) {
424+ if ( ! this . featuredCarousel || ! this . featuredArticles || this . featuredArticles . length === 0 ) {
425+ return ;
426+ }
427+
428+ // Show featured section
429+ const featuredSection = document . getElementById ( 'featured-posts-section' ) ;
430+ if ( featuredSection ) {
431+ featuredSection . style . display = 'block' ;
432+ }
433+
434+ // Clear carousel
435+ this . featuredCarousel . innerHTML = '' ;
436+
437+ // Render each featured article
438+ this . featuredArticles . forEach ( article => {
439+ const featuredCard = this . createFeaturedCard ( article ) ;
440+ this . featuredCarousel . appendChild ( featuredCard ) ;
441+ } ) ;
442+ }
443+
444+ /**
445+ * Create featured article card
446+ * @param {Object } article - Article data
447+ * @returns {HTMLElement } Featured card element
448+ */
449+ createFeaturedCard ( article ) {
450+ const cardElement = document . createElement ( 'article' ) ;
451+ cardElement . className = 'featured-card' ;
452+
453+ const isExternal = article . url . startsWith ( 'http' ) ;
454+ const linkAttrs = isExternal ? 'target="_blank" rel="noopener noreferrer"' : '' ;
455+
456+ const tagsHTML = article . tags
457+ . slice ( 0 , 3 )
458+ . map ( tag => `<span class="tag">${ this . escapeHtml ( tag ) } </span>` )
459+ . join ( '' ) ;
460+
461+ cardElement . innerHTML = `
462+ <div class="featured-star">
463+ <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
464+ <path d="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z"/>
465+ </svg>
466+ </div>
467+ <a href="${ this . escapeHtml ( article . url ) } " ${ linkAttrs } class="featured-link">
468+ <div class="featured-image">
469+ <img src="${ this . escapeHtml ( article . image ) } "
470+ alt="${ this . escapeHtml ( article . title ) } "
471+ loading="lazy">
472+ </div>
473+ <div class="featured-content">
474+ <div class="featured-meta">
475+ <time datetime="${ this . escapeHtml ( article . publishDate ) } ">
476+ ${ this . formatDate ( article . publishDate ) }
477+ </time>
478+ <span class="read-time">${ this . escapeHtml ( article . readTime ) } </span>
479+ </div>
480+ <h3>${ this . escapeHtml ( article . title ) } </h3>
481+ <p>${ this . escapeHtml ( article . description ) } </p>
482+ <div class="featured-tags">
483+ ${ tagsHTML }
484+ </div>
485+ </div>
486+ </a>
487+ ` ;
488+
489+ return cardElement ;
490+ }
491+
492+ /**
493+ * Setup carousel navigation
494+ */
495+ setupCarouselNavigation ( ) {
496+ const prevBtn = document . getElementById ( 'carousel-prev' ) ;
497+ const nextBtn = document . getElementById ( 'carousel-next' ) ;
498+
499+ if ( ! prevBtn || ! nextBtn || ! this . featuredCarousel ) return ;
500+
501+ // Check if navigation is needed
502+ this . checkCarouselNavigation ( prevBtn , nextBtn ) ;
503+
504+ // Scroll carousel on button click
505+ prevBtn . addEventListener ( 'click' , ( ) => {
506+ this . featuredCarousel . scrollBy ( {
507+ left : - 280 ,
508+ behavior : 'smooth'
509+ } ) ;
510+ } ) ;
511+
512+ nextBtn . addEventListener ( 'click' , ( ) => {
513+ this . featuredCarousel . scrollBy ( {
514+ left : 280 ,
515+ behavior : 'smooth'
516+ } ) ;
517+ } ) ;
518+
519+ // Update button states on scroll
520+ this . featuredCarousel . addEventListener ( 'scroll' , ( ) => {
521+ this . updateCarouselButtons ( prevBtn , nextBtn ) ;
522+ } ) ;
523+
524+ // Re-check on window resize
525+ window . addEventListener ( 'resize' , ( ) => {
526+ this . checkCarouselNavigation ( prevBtn , nextBtn ) ;
527+ } ) ;
528+
529+ // Initial button state
530+ this . updateCarouselButtons ( prevBtn , nextBtn ) ;
531+ }
532+
533+ /**
534+ * Check if carousel navigation buttons should be visible
535+ */
536+ checkCarouselNavigation ( prevBtn , nextBtn ) {
537+ if ( ! this . featuredCarousel ) return ;
538+
539+ // Wait for images to load before checking
540+ setTimeout ( ( ) => {
541+ const isOverflowing = this . featuredCarousel . scrollWidth > this . featuredCarousel . clientWidth ;
542+
543+ if ( isOverflowing ) {
544+ prevBtn . style . display = 'flex' ;
545+ nextBtn . style . display = 'flex' ;
546+ } else {
547+ prevBtn . style . display = 'none' ;
548+ nextBtn . style . display = 'none' ;
549+ }
550+ } , 100 ) ;
551+ }
552+
553+ /**
554+ * Update carousel button states based on scroll position
555+ */
556+ updateCarouselButtons ( prevBtn , nextBtn ) {
557+ if ( ! this . featuredCarousel ) return ;
558+
559+ const scrollLeft = this . featuredCarousel . scrollLeft ;
560+ const maxScroll = this . featuredCarousel . scrollWidth - this . featuredCarousel . clientWidth ;
561+
562+ // Disable/enable previous button
563+ if ( scrollLeft <= 0 ) {
564+ prevBtn . disabled = true ;
565+ } else {
566+ prevBtn . disabled = false ;
567+ }
568+
569+ // Disable/enable next button
570+ if ( scrollLeft >= maxScroll - 1 ) {
571+ nextBtn . disabled = true ;
572+ } else {
573+ nextBtn . disabled = false ;
574+ }
575+ }
406576}
407577
408578// Initialize blog loader when DOM is ready
0 commit comments