PageSourceSearch

https://pressthink.org/j/rosen-archive/frontend/components/FeaturedSection.js?v=3.8.36

js pressthink.org collected 2026-10-02 04:26:28 UTC 5,228 bytes, 138 lines download raw bytes

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.