822f4266f6
Reorganizes the /docs surface from a single DOCS.md route into a hub with a left-side flyout nav and three new sub-routes for the on-site sessions browser (roadmap item #30): /docs → redirect to /docs/user-guide /docs/user-guide → existing DOCS.md content /docs/sessions → redirect to /docs/sessions/about /docs/sessions/about → README.md of ohm-session-history /docs/sessions/<NNNN> → per-session transcript index /docs/sessions/<NNNN>/<f> → per-transcript view The flyout is a persistent left sidebar on desktop and a slide-out drawer on mobile (toggled by a ☰ button in the docs header). Its session list is driven by the /api/docs/sessions/manifest fetch — loading shows a skeleton; 502 shows an inline retry; empty manifest shows only the "About" row. Each sub-route owns its own empty-state / error handling: - 404 transcripts render "This transcript isn't published yet" with a link back to the parent session index, no JS crash. - 502 (gitea unreachable) renders a retry button. - Manifest 404 is mapped to {} server-side so the flyout renders cleanly with no error banner when no sessions are published yet. Analytics (per SPEC §21): - new EVENTS.DOC_VIEWED ("Doc Viewed") fires on each sub-route mount with `section`: 'user-guide' | 'sessions/about' | 'sessions/<NNNN>' | 'sessions/<NNNN>/<filename>'. - every interactive nav element carries aria-label + data-amp-track-name so autocapture rows are readable. The existing v0.14.0 Docs.jsx component is dropped — its content moved verbatim into DocsUserGuide.jsx; the new layout subsumes the back-button + signed-out home affordances it used to carry. All four new sub-routes reuse the existing MarkdownPreview renderer (marked + mermaid lazy-load) so no second markdown library lands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
127 lines
3.9 KiB
React
127 lines
3.9 KiB
React
// DocsSessionIndex.jsx — v0.19.0 / roadmap item #30.
|
|
//
|
|
// Per-session index page at `/docs/sessions/:nnnn`. Lists every
|
|
// transcript published under the session's NNNN/ folder, linked to
|
|
// the per-transcript view.
|
|
//
|
|
// The transcript list comes from `/api/docs/sessions/:nnnn/index`,
|
|
// which the framework derives via the gitea contents API (see
|
|
// backend/app/docs_sessions.fetch_session_index). We also read the
|
|
// session's `title` from the manifest fetch so the page header
|
|
// matches the flyout nav entry.
|
|
|
|
import { useEffect, useState, useCallback } from 'react'
|
|
import { Link, useParams } from 'react-router-dom'
|
|
import { getSessionsManifest, getSessionIndex } from '../api.js'
|
|
import { EVENTS, track } from '../lib/analytics'
|
|
|
|
export default function DocsSessionIndex() {
|
|
const { nnnn } = useParams()
|
|
const [title, setTitle] = useState('')
|
|
const [files, setFiles] = useState([])
|
|
const [status, setStatus] = useState('loading') // loading | ok | notfound | error
|
|
const [reloadTick, setReloadTick] = useState(0)
|
|
|
|
useEffect(() => {
|
|
track(EVENTS.DOC_VIEWED, { section: `sessions/${nnnn}` })
|
|
}, [nnnn])
|
|
|
|
// Title from manifest — cheap, manifest is cached server-side.
|
|
useEffect(() => {
|
|
let active = true
|
|
getSessionsManifest()
|
|
.then(payload => {
|
|
if (!active) return
|
|
const entry = payload && payload[nnnn]
|
|
setTitle((entry && entry.title) || '')
|
|
})
|
|
.catch(() => {
|
|
// Title is decorative; failure to load just leaves the header
|
|
// showing the bare NNNN. The transcript list fetch below is
|
|
// the load-bearing one.
|
|
})
|
|
return () => { active = false }
|
|
}, [nnnn])
|
|
|
|
// File list from the per-session index endpoint.
|
|
useEffect(() => {
|
|
let active = true
|
|
setStatus('loading')
|
|
getSessionIndex(nnnn)
|
|
.then(payload => {
|
|
if (!active) return
|
|
setFiles((payload && payload.files) || [])
|
|
setStatus('ok')
|
|
})
|
|
.catch(e => {
|
|
if (!active) return
|
|
if (e.status === 404) {
|
|
setStatus('notfound')
|
|
} else {
|
|
setStatus('error')
|
|
}
|
|
})
|
|
return () => { active = false }
|
|
}, [nnnn, reloadTick])
|
|
|
|
const retry = useCallback(() => setReloadTick(t => t + 1), [])
|
|
|
|
const header = title ? `${nnnn} — ${title}` : `Session ${nnnn}`
|
|
|
|
return (
|
|
<article className="docs-article">
|
|
<h1 className="docs-article-title">{header}</h1>
|
|
{status === 'loading' && <p className="muted">Loading…</p>}
|
|
{status === 'notfound' && (
|
|
<div className="docs-empty">
|
|
<p>
|
|
No transcripts have been published for this session yet.{' '}
|
|
<Link
|
|
to="/docs/sessions/about"
|
|
aria-label="About sessions"
|
|
data-amp-track-name="Docs Session Empty About Link"
|
|
>
|
|
About sessions
|
|
</Link>
|
|
</p>
|
|
</div>
|
|
)}
|
|
{status === 'error' && (
|
|
<div className="docs-error" role="alert">
|
|
<p>Couldn't reach the session-history repo.</p>
|
|
<button
|
|
type="button"
|
|
onClick={retry}
|
|
aria-label="Retry"
|
|
data-amp-track-name="Docs Session Index Retry"
|
|
>
|
|
Try again
|
|
</button>
|
|
</div>
|
|
)}
|
|
{status === 'ok' && files.length === 0 && (
|
|
<div className="docs-empty">
|
|
<p>This session has no transcripts published.</p>
|
|
</div>
|
|
)}
|
|
{status === 'ok' && files.length > 0 && (
|
|
<ul className="docs-session-files">
|
|
{files.map(f => (
|
|
<li key={f}>
|
|
<Link
|
|
to={`/docs/sessions/${nnnn}/${f}`}
|
|
aria-label={`Open transcript ${f}`}
|
|
data-amp-track-name="Docs Session Transcript Open"
|
|
data-amp-track-session={nnnn}
|
|
data-amp-track-filename={f}
|
|
>
|
|
{f}
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</article>
|
|
)
|
|
}
|