PageSourceSearch

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

js pressthink.org collected 2026-10-02 04:26:44 UTC 9,491 bytes, 237 lines download raw bytes

1import { html } from '../html.js?v=3.8.36';
2import { ChevronLeft, ChevronRight, FolderOpen, SearchX } from 'lucide-react';
3import { COLORS } from '../constants.js?v=3.8.36';
4import { hashString } from '../services/archiveService.js?v=3.8.36';
5import { recordNeedsReview } from '../utils/needsReview.js?v=3.8.36';
6import { LEXICAL_SIGNAL, presentSearchSignal } from '../utils/searchRanking.js?v=3.8.36';
7import { canonicalRecordUrl } from '../utils/recordDeepLink.js?v=3.8.36';
8import LoadingQuotes from './LoadingQuotes.js?v=3.8.36';
9
10const Highlight = ({ text, term }) => {
11  if (!term || term.length < 2) return html`<span>${text}</span>`;
12  const parts = text.split(new RegExp(`(${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'));
13  return html`
14    <span>
15      ${parts.map((part, index) => (
16        part.toLowerCase() === term.toLowerCase()
17          ? html`<mark key=${index}>${part}</mark>`
18          : part
19      ))}
20    </span>
21  `;
22};
23
24/**
25 * The canonical archive-result renderer shared by the standard archive and
26 * the optional desktop adapter. Filtering, sorting, pagination, and record
27 * selection remain owned by App; this component only presents those results.
28 */
29const ArchiveResults = ({
30  compact = false,
31  errorPanel = null,
32  loading,
33  filteredRecords,
34  paginatedRecords,
35  folderGroups,
36  viewMode,
37  searchTerm,
38  // Map of record id to its hybrid-search provenance chip: 'kw', 'sem', or
39  // 'kw·sem' (#279). Null unless semantic search is on, so a plain keyword
40  // search does not label every card. A record the ranked legs never reached
41  // still matched the typed words by substring, so it gets the keyword chip:
42  // while chips are on, every card carries one.
43  searchSignals = null,
44  currentPage,
45  totalPages,
46  onSelectRecord,
47  onOpenFolder,
48  onPageChange,
49  onClearFilters,
50  // Brief stuck-record skip for the broken-record extra (#754). Off by
51  // default, and the CSS drops the movement under reduced motion.
52  skipping = false,
53}) => {
54  const openCard = (event, recordId) => {
55    if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
56    event.preventDefault();
57    onSelectRecord(recordId);
58  };
59
60  return html`
61    <div className=${`${compact ? 'archive-results archive-results--compact desktop-archive-results' : 'archive-results'}${skipping ? ' archive-results--skipping' : ''}`}>
62      ${errorPanel}
63
64      ${!errorPanel && loading && html`<${LoadingQuotes} />`}
65
66      ${!errorPanel && !loading && filteredRecords.length === 0 && html`
67        <div className="archive-empty-state">
68          <${SearchX} className="archive-empty-state__icon" aria-hidden="true" />
69          <p className="archive-section-label">Search result</p>
70          <h3>No records found</h3>
71          <p>Try adjusting your search terms or filters.</p>
72          <button
73            type="button"
74            onClick=${onClearFilters}
75            className="archive-action archive-action--secondary"
76          >
77            Clear all filters
78          </button>
79        </div>
80      `}
81
82      ${!errorPanel && !loading && viewMode === 'grid' && html`
83        <div className=${compact
84          ? 'desktop-archive-card-grid'
85          : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6'}>
86          ${paginatedRecords.map((item) => {
87            const primaryCategory = item.categories[0] || 'Uncategorized';
88            const colorIndex = hashString(primaryCategory) % COLORS.length;
89            const theme = COLORS[colorIndex];
90            const signal = searchSignals
91              ? (searchSignals.get(item.id) || LEXICAL_SIGNAL)
92              : null;
93            const signalCopy = signal ? presentSearchSignal(signal) : null;
94
95            return html`
96              <article
97                key=${item.id}
98                className="archive-record-card"
99              >
100                <span
101                  className="archive-record-card__accent"
102                  style=${{ backgroundColor: theme.text }}
103                  aria-hidden="true"
104                />
105                <a
106                  href=${canonicalRecordUrl(window.location.href, item.id)}
107                  aria-label=${`Open record: ${item.title}`}
108                  onClick=${event => openCard(event, item.id)}
109                  className="archive-record-card__body"
110                >
111                  <div className="archive-record-card__meta">
112                    <span className="archive-record-card__publication">${item.pub}</span>
113                    <span className="archive-record-card__date">${item.date}</span>
114                  </div>
115
116                  <h3 className="archive-record-card__title">
117                    <${Highlight} text=${item.title} term=${searchTerm} />
118                  </h3>
119
120                  <p className="archive-record-card__summary">
121                    <${Highlight} text=${item.summaryPreview || item.summary || ''} term=${searchTerm} />
122                  </p>
123
124                  <div className="archive-record-card__labels">
125                    ${signal && html`
126                      <span
127                        className="archive-record-card__label archive-record-card__label--signal"
128                        title=${signalCopy.description}
129                      >
130                        ${signalCopy.label}
131                      </span>
132                    `}
133                    ${item.categories.slice(0, 2).map((category, index) => html`
134                      <span
135                        key=${category}
136                        className="archive-record-card__label"
137                        style=${index === 0 ? {
138                          backgroundColor: theme.bg,
139                          borderColor: theme.border,
140                          color: theme.text,
141                        } : undefined}
142                      >
143                        ${category}
144                      </span>
145                    `)}
146                    ${item.categories.length > 2 && html`
147                      <span className="archive-record-card__label">
148                        +${item.categories.length - 2} more
149                      </span>
150                    `}
151                    ${recordNeedsReview(item) && html`
152                      <span
153                        className="archive-record-card__label archive-record-card__label--review"
154                        style=${{ backgroundColor: '#fffbeb', color: '#b45309' }}
155                        title="Auto-submitted; pending a human review pass"
156                      >
157                        needs review
158                      </span>
159                    `}
160                  </div>
161                </a>
162              </article>
163            `;
164          })}
165        </div>
166      `}
167
168      ${!errorPanel && !loading && viewMode === 'folder' && html`
169        <div className=${compact
170          ? 'desktop-archive-folder-grid'
171          : 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6'}>
172          ${folderGroups.map((group) => {
173            const colorIndex = hashString(group.name) % COLORS.length;
174            const theme = COLORS[colorIndex];
175
176            return html`
177              <button
178                type="button"
179                key=${group.name}
180                onClick=${() => onOpenFolder(group.name)}
181                className="archive-folder-card"
182              >
183                <span
184                  className="archive-folder-tab archive-folder-card__tab"
185                  style=${{
186                    '--archive-folder-tab-fill': theme.bg,
187                    '--archive-folder-tab-edge': theme.border,
188                  }}
189                >
190                  <span
191                    className="archive-folder-card__count"
192                    style=${{ color: theme.text }}
193                  >
194                    ${group.count} records
195                  </span>
196                </span>
197                <span className="archive-folder-card__body">
198                  <span className="archive-folder-card__accent" style=${{ backgroundColor: theme.text }}></span>
199                  <span className="archive-folder-card__name">
200                    ${group.name}
201                  </span>
202                  <${FolderOpen} className="archive-folder-card__icon" aria-hidden="true" />
203                </span>
204              </button>
205            `;
206          })}
207        </div>
208      `}
209
210      ${!loading && !errorPanel && viewMode === 'grid' && totalPages > 1 && html`
211        <nav className="archive-pagination" aria-label="Archive results pages">
212          <button
213            type="button"
214            onClick=${() => onPageChange(currentPage - 1)}
215            disabled=${currentPage === 1}
216            className="archive-action archive-action--quiet archive-pagination__button"
217            aria-label="Previous results page"
218          >
219            <${ChevronLeft} className="w-5 h-5" aria-hidden="true" />
220          </button>
221          <span className="archive-pagination__status">Page ${currentPage} of ${totalPages}</span>
222          <button
223            type="button"
224            onClick=${() => onPageChange(currentPage + 1)}
225            disabled=${currentPage === totalPages}
226            className="archive-action archive-action--quiet archive-pagination__button"
227            aria-label="Next results page"
228          >
229            <${ChevronRight} className="w-5 h-5" aria-hidden="true" />
230          </button>
231        </nav>
232      `}
233    </div>
234  `;
235};
236
237export default ArchiveResults;

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.