v0.19.0 frontend: /docs/* route tree + flyout nav + sessions browser
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>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
// 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user