PageSourceSearch

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

js pressthink.org collected 2026-10-02 04:26:40 UTC 9,753 bytes, 186 lines download raw bytes

1import { useEffect, useMemo, useState } from 'react';
2import { html } from '../html.js?v=3.8.36';
3import { AlertCircle, Search } from 'lucide-react';
4import { buildWikiPageIndex, fetchWikiData, filterWikiPages, findWikiPageBySlug, normalizeWikiPages, parseWikiHash, wikiPageHref } from '../services/wikiService.js?v=3.8.36';
5
6const KIND_LABELS = {
7  concept: 'Concepts',
8  entity: 'Entities',
9  topic: 'Topics and tags'
10};
11
12const RelatedList = ({ title, slugs, pages }) => {
13  if (!slugs?.length) return null;
14  return html`
15    <section>
16      <h3 className="font-display text-stone-900 text-lg mb-3">${title}</h3>
17      <div className="flex flex-wrap gap-2">
18        ${slugs.map(slug => {
19          const page = findWikiPageBySlug(pages, slug);
20          return html`
21            <a key=${slug} href=${wikiPageHref(slug)} className="px-3 py-1.5 rounded-full border border-stone-200 bg-stone-50 text-xs font-bold text-stone-700 hover:border-stone-400 hover:bg-white transition-colors">
22              ${page?.title || slug}
23            </a>
24          `;
25        })}
26      </div>
27    </section>
28  `;
29};
30
31const WikiDetail = ({ page, pages, onBack }) => html`
32  <article className="max-w-4xl mx-auto w-full">
33    <button onClick=${onBack} className="text-xs font-bold text-stone-500 hover:text-stone-900 mb-6">← Back to wiki index</button>
34
35    <div className="bg-white border border-stone-200 rounded-sm shadow-sm overflow-hidden">
36      <div className="border-b border-stone-200 p-6 md:p-8 bg-stone-50">
37        <div className="flex flex-wrap gap-2 mb-4">
38          <span className="text-[10px] uppercase tracking-wider font-bold px-2 py-1 bg-stone-900 text-white rounded-sm">${page.kind}</span>
39        </div>
40        <h1 className="font-display text-3xl md:text-5xl text-stone-900 mb-4">${page.title}</h1>
41        <p className="text-lg text-stone-700 leading-relaxed max-w-3xl">${page.summary}</p>
42      </div>
43
44      <div className="p-6 md:p-8 grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-8">
45        <div className="space-y-8">
46          ${page.body.map(block => html`
47            <section key=${block.heading}>
48              <h2 className="font-display text-2xl text-stone-900 mb-3">${block.heading}</h2>
49              <p className="text-stone-700 leading-relaxed">${block.text}</p>
50            </section>
51          `)}
52
53          <${RelatedList} title="Related concepts" slugs=${page.relatedConcepts} pages=${pages} />
54          <${RelatedList} title="Related entities" slugs=${page.relatedEntities} pages=${pages} />
55
56          <section>
57            <h2 className="font-display text-2xl text-stone-900 mb-3">Sources and references</h2>
58            ${page.references.length ? html`
59              <ul className="space-y-2">
60                ${page.references.map(ref => html`
61                  <li key=${ref.url} className="text-sm">
62                    <a href=${ref.url} target="_blank" rel="noopener noreferrer" className="text-stone-900 font-bold underline decoration-stone-300 underline-offset-4 hover:decoration-stone-700">${ref.label}</a>
63                  </li>
64                `)}
65              </ul>
66            ` : html`<p className="text-sm text-stone-500">No public references have been attached yet.</p>`}
67          </section>
68        </div>
69
70        <aside className="space-y-4">
71          <div className="border border-stone-200 bg-stone-50 rounded-sm p-4">
72            <h2 className="font-display text-lg text-stone-900 mb-3">Page facts</h2>
73            <dl className="space-y-3 text-xs text-stone-600">
74              <div><dt className="font-bold text-stone-900">Last updated</dt><dd>${page.lastUpdated}</dd></div>
75              <div><dt className="font-bold text-stone-900">Contributors</dt><dd>${page.contributors.join(', ')}</dd></div>
76            </dl>
77          </div>
78          ${page.aliases.length > 0 ? html`
79            <div className="border border-stone-200 bg-white rounded-sm p-4">
80              <h2 className="font-display text-lg text-stone-900 mb-3">Aliases</h2>
81              <div className="flex flex-wrap gap-2">
82                ${page.aliases.map(alias => html`<span key=${alias} className="text-xs px-2 py-1 bg-stone-100 text-stone-700 rounded-sm">${alias}</span>`)}
83              </div>
84            </div>
85          ` : null}
86        </aside>
87      </div>
88    </div>
89  </article>
90`;
91
92const WikiPage = ({ initialSlug = null }) => {
93  const [pages, setPages] = useState([]);
94  const [loading, setLoading] = useState(true);
95  const [error, setError] = useState(null);
96  const [query, setQuery] = useState('');
97  const [kind, setKind] = useState('all');
98  const [activeSlug, setActiveSlug] = useState(initialSlug);
99  const [notFound, setNotFound] = useState(false);
100
101  useEffect(() => {
102    const syncSlug = () => {
103      const parsed = parseWikiHash(window.location.hash);
104      setActiveSlug(parsed.slug);
105      setNotFound(parsed.notFound);
106    };
107    syncSlug();
108    window.addEventListener('hashchange', syncSlug);
109    return () => window.removeEventListener('hashchange', syncSlug);
110  }, []);
111
112  useEffect(() => {
113    fetchWikiData()
114      .then(data => setPages(normalizeWikiPages(data)))
115      .catch(err => {
116        console.error(err);
117        setError('Failed to load wiki seed data.');
118      })
119      .finally(() => setLoading(false));
120  }, []);
121
122  const visiblePages = useMemo(() => filterWikiPages(pages, { query, kind }), [pages, query, kind]);
123  const pageIndex = useMemo(() => buildWikiPageIndex(pages), [pages]);
124  const selectedPage = activeSlug ? (pageIndex.get(activeSlug) || null) : null;
125  const counts = useMemo(() => pages.reduce((acc, page) => ({ ...acc, [page.kind]: (acc[page.kind] || 0) + 1 }), {}), [pages]);
126
127  if (loading) return html`<div className="py-20 text-center text-stone-500">Loading wiki...</div>`;
128  if (error) return html`<div className="py-20 text-center text-red-600">${error}</div>`;
129  if (selectedPage) return html`<${WikiDetail} page=${selectedPage} pages=${pages} onBack=${() => { window.location.hash = 'wiki'; }} />`;
130  if (activeSlug || notFound) return html`
131    <div className="max-w-3xl mx-auto w-full text-center py-20 border border-dashed border-stone-300 rounded-sm bg-white">
132      <${AlertCircle} className="w-10 h-10 mx-auto text-amber-500 mb-4" />
133      <h1 className="font-display text-3xl text-stone-900 mb-3">Wiki page not found</h1>
134      <p className="text-sm text-stone-600 mb-6">${activeSlug ? `No wiki page exists for ${activeSlug}.` : 'That wiki link is not valid.'} Only published wiki pages are available.</p>
135      <button onClick=${() => { window.location.hash = 'wiki'; }} className="px-4 py-2 bg-stone-900 text-white rounded-sm text-sm font-bold hover:bg-stone-700">Back to wiki index</button>
136    </div>
137  `;
138
139  return html`
140    <div className="max-w-6xl mx-auto w-full space-y-8">
141      <section className="bg-white border border-stone-200 rounded-sm p-6 md:p-8 shadow-sm">
142        <h1 className="font-display text-4xl md:text-6xl text-stone-900 mb-4">Archive wiki</h1>
143        <p className="text-stone-700 leading-relaxed max-w-3xl">
144          The key ideas behind the archive, each one drawn from Jay Rosen's 1986 dissertation, The Impossible Press. Browse the concepts, follow the links between them, and trace every claim back to the text.
145        </p>
146      </section>
147
148      <section className="bg-stone-50 border border-stone-200 rounded-sm p-4 md:p-5">
149        <div className="flex flex-col lg:flex-row gap-4 lg:items-center lg:justify-between">
150          <label className="relative flex-grow">
151            <span className="sr-only">Search wiki pages</span>
152            <${Search} className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-stone-400" />
153            <input value=${query} onInput=${e => setQuery(e.target.value)} placeholder="Search wiki pages..." className="w-full pl-10 pr-4 py-2 border border-stone-300 rounded-sm bg-white text-sm focus:outline-none focus:ring-2 focus:ring-stone-400" />
154          </label>
155          <div className="flex flex-wrap gap-2">
156            ${['all', 'concept', 'entity', 'topic'].map(option => html`
157              <button key=${option} onClick=${() => setKind(option)} className=${`px-3 py-2 rounded-sm border text-xs font-bold transition-colors ${kind === option ? 'bg-stone-900 text-white border-stone-900' : 'bg-white text-stone-700 border-stone-200 hover:border-stone-400'}`}>
158                ${option === 'all' ? `All (${pages.length})` : `${KIND_LABELS[option]} (${counts[option] || 0})`}
159              </button>
160            `)}
161          </div>
162        </div>
163      </section>
164
165      <section className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-5">
166        ${visiblePages.map(page => html`
167          <a key=${page.id} href=${wikiPageHref(page.slug)} className="group bg-white border border-stone-200 rounded-sm p-5 shadow-sm hover:border-stone-400 hover:shadow-md transition-all">
168            <span className="text-[10px] uppercase tracking-wider font-bold text-stone-500 block mb-3">${page.kind}</span>
169            <h2 className="font-display text-2xl text-stone-900 mb
169-2 group-hover:underline decoration-stone-300 underline-offset-4">${page.title}</h2>
170            <p className="text-sm text-stone-600 leading-relaxed mb-4">${page.summary}</p>
171            <span className="text-xs text-stone-400">Updated ${page.lastUpdated}</span>
172          </a>
173        `)}
174      </section>
175
176      ${visiblePages.length === 0 && html`
177        <div className="text-center py-16 border border-dashed border-stone-300 rounded-sm bg-white">
178          <${Search} className="w-8 h-8 mx-auto text-stone-300 mb-3" />
179          <p className="text-stone-500 text-sm">No wiki pages match those filters.</p>
180        </div>
181      `}
182    </div>
183  `;
184};
185
186export default WikiPage;

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.