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.