1import { useCallback, useEffect, useState } from 'react'
2import ChapterView from './ui/ChapterView.tsx'
3import LibraryView from './ui/LibraryView.tsx'
4import { CHAPTERS } from './chapters/index.ts'
5import { PARTS } from './ui/types.ts'
7/** The introduction is front matter, so the chapter numbers start after it. */
8const NUMBERED = CHAPTERS.filter((c) => c.part !== 'Introduction')
10function readHash(): string {
11 const h = window.location.hash.replace(/^#\/?/, '')
12 return h === '' ? CHAPTERS[0].id : h
13}
15export default function App() {
16 const [route, setRoute] = useState(readHash)
17 useEffect(() => {
18 const on = () => setRoute(readHash())
19 window.addEventListener('hashchange', on)
20 return () => window.removeEventListener('hashchange', on)
21 }, [])
23 const go = useCallback((id: string) => {
24 window.location.hash = `/${id}`
25 window.scrollTo({ top: 0 })
26 }, [])
28 const chapter = CHAPTERS.find((c) => c.id === route)
29 const index = CHAPTERS.findIndex((c) => c.id === route)
31 return (
32 <div className="app">
33 <nav className="sidebar">
34 <div className="brand">
35 <div className="brand-title">Waves and sound</div>
36 <div className="brand-sub">the wave equation, and what its errors sound like</div>
37 </div>
38 {PARTS.map((part) => (
39 <div className="nav-part" key={part}>
40 <div className="nav-part-title">{part}</div>
41 <ol>
42 {CHAPTERS.filter((c) => c.part === part).map((c) => (
43 <li key={c.id}>
44 <button className={c.id === route ? 'nav-item active' : 'nav-item'} onClick={() => go(c.id)}>
45 <span className="nav-num">{NUMBERED.indexOf(c) + 1 || ''}</span>
46 {c.nav}
47 </button>
48 </li>
49 ))}
50 </ol>
51 </div>
52 ))}
53 <div className="nav-part">
54 <div className="nav-part-title">Reference</div>
55 <ol>
56 <li>
57 <button className={route === 'library' ? 'nav-item active' : 'nav-item'} onClick={() => go('library')}>
58 <span className="nav-num" />
59 shared MATLAB library
60 </button>
61 </li>
62 </ol>
63 </div>
64 <div className="nav-foot">
65 MATLAB syntax runs in your browser through <a href="https://numbl.org">numbl</a>. Nothing is installed and
66 nothing is sent anywhere.
67 </div>
68 </nav>
70 <main className="main">
71 {route === 'library' ? (
72 <LibraryView />
73 ) : chapter ? (
74 <>
75 <ChapterView chapter={chapter} />
76 <nav className="pager">
77 {index > 0 && <button onClick={() => go(CHAPTERS[index - 1].id)}>← {CHAPTERS[index - 1].nav}</button>}
78 <span className="lab-bar-gap" />
79 {index < CHAPTERS.length - 1 && (
80 <button onClick={() => go(CHAPTERS[index + 1].id)}>{CHAPTERS[index + 1].nav} →</button>
81 )}
82 </nav>
83 </>
84 ) : (
85 <p className="lab-error">no such chapter: {route}</p>
86 )}
87 </main>
89 {/* Sits above every page and takes no clicks, so it can neither be
90 dismissed nor get in the way of a control underneath it. */}
91 <aside className="ai-disclaimer" role="note">
92 <strong>Note:</strong> written by AI, not fully checked by a human.
93 </aside>
94 </div>
95 )
96}