1 2import { useState, useEffect, useCallback } from 'react'; 3import { html } from '../html.js?v=3.8.36'; 4import { ArrowRight, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Book, GraduationCap, MessageCircle, PenTool, FileText, Sparkles } from 'lucide-react'; 5import { FEATURED_WORKS } from '../constants.js?v=3.8.36'; 6 7const FeaturedSection = () => { 8 const [startIndex, setStartIndex] = useState(0); 9 const [isPaused, setIsPaused] = useState(false); 10 const [isExpanded, setIsExpanded] = useState(true); 11 12 const ITEMS_TO_SHOW = 3; 13 const ROTATION_INTERVAL = 6000; 14 15 const nextSlide = useCallback(() => { 16 setStartIndex((prev) => (prev + 1) % FEATURED_WORKS.length); 17 }, []); 18 19 const previousSlide = useCallback(() => { 20 setStartIndex((prev) => (prev - 1 + FEATURED_WORKS.length) % FEATURED_WORKS.length); 21 }, []); 22 23 useEffect(() => { 24 const prefersReducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches; 25 if (prefersReducedMotion || isPaused || !isExpanded || FEATURED_WORKS.length <= ITEMS_TO_SHOW) return; 26 27 const interval = setInterval(nextSlide, ROTATION_INTERVAL); 28 return () => clearInterval(interval); 29 }, [isPaused, nextSlide, isExpanded]); 30 31 const visibleWorks = [ 32 FEATURED_WORKS[startIndex % FEATURED_WORKS.length], 33 FEATURED_WORKS[(startIndex + 1) % FEATURED_WORKS.length], 34 FEATURED_WORKS[(startIndex + 2) % FEATURED_WORKS.length] 35 ]; 36 37 const getIconForType = (type) => { 38 switch(type) { 39 case 'PhD Dissertation': return html`<${GraduationCap} className="w-3 h-3 mr-1" />`; 40 case 'Book': return html`<${Book} className="w-3 h-3 mr-1" />`; 41 case 'Key Concept': return html`<${MessageCircle} className="w-3 h-3 mr-1" />`; 42 case 'Blog': return html`<${PenTool} className="w-3 h-3 mr-1" />`; 43 case 'Critique': return html`<${FileText} className="w-3 h-3 mr-1" />`; 44 default: return html`<${Sparkles} className="w-3 h-3 mr-1" />`; 45 } 46 } 47 48 return html` 49 <section 50 className="archive-featured" 51 onMouseEnter=${() => setIsPaused(true)} 52 onMouseLeave=${event => { 53 if (!event.currentTarget.contains(document.activeElement)) setIsPaused(false); 54 }} 55 onFocusCapture=${() => setIsPaused(true)} 56 onBlurCapture=${event => { 57 if (!event.currentTarget.contains(event.relatedTarget)) setIsPaused(false); 58 }} 59 aria-labelledby="archive-featured-title" 60 > 61 <div className="archive-featured__header"> 62 <button 63 type="button" 64 onClick=${() => setIsExpanded(!isExpanded)} 65 className="archive-featured__toggle" 66 aria-expanded=${isExpanded} 67 aria-controls="archive-featured-grid" 68 > 69 <h2 id="archive-featured-title">Read</h2> 70 <span className="archive-featured__description">Curated highlights</span> 71 <span className="archive-featured__chevron"> 72 ${isExpanded ? html`<${ChevronUp} className="w-5 h-5" />` : html`<${ChevronDown} className="w-5 h-5" />`} 73 </span> 74 </button> 75 76 ${isExpanded && html` 77 <div className="archive-featured__controls" aria-label="Choose featured set"> 78 <button 79 type="button" 80 onClick=${previousSlide} 81 className="archive-featured__control" 82 aria-label="Previous featured work" 83 > 84 <${ChevronLeft} aria-hidden="true" /> 85 </button> 86 <span className="archive-featured__position"> 87 ${startIndex + 1} of ${FEATURED_WORKS.length} 88 </span> 89 <button 90 type="button" 91 onClick=${nextSlide} 92 className="archive-featured__control" 93 aria-label="Next featured work" 94 > 95 <${ChevronRight} aria-hidden="true" /> 96 </button> 97 </div> 98 `} 99 </div> 100 101 ${isExpanded && html` 102 <div id="archive-featured-grid" className="archive-featured__grid"> 103 ${visibleWorks.map((work) => html` 104 <a 105 key=${`${work.id}-${startIndex}`} 106 href=${work.link} 107 target="_blank" 108 rel="noreferrer" 109 className="archive-featured-card" 110 aria-label=${`Read ${work.title}`} 111 > 112 <div className="archive-featured-card__media"> 113 <img src=${work.image} alt="" />
114 <span className="archive-featured-card__label"> 115 ${getIconForType(work.type)} ${work.type} 116 </span> 117 </div> 118 119 <div className="archive-featured-card__body"> 120 <h3> 121 ${work.title} 122 </h3> 123 <p>${work.description}</p> 124 125 <div className="archive-featured-card__read"> 126 <span>Read</span> 127 <${ArrowRight} className="w-4 h-4" aria-hidden="true" /> 128 </div> 129 </div> 130 </a> 131 `)} 132 </div> 133 `} 134 </section> 135 `; 136}; 137 138export default FeaturedSection;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.