Files
spelljammer-ships/web/src/pages/RulesPage.tsx
Bas van Rossem 69e53ed695 feat(web): add search to Rules page
Filter sections by typing in the search box — matches against both
section titles and content. Shows result count and a "no results"
message. Expand/Collapse All buttons respect the filtered set.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 17:28:22 +01:00

92 lines
3.0 KiB
TypeScript

import { useState, useMemo } from 'react';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import TopBar from '../components/layout/TopBar';
import PageContainer from '../components/layout/PageContainer';
import { battleReferenceSections } from '../rules/battle-reference';
export default function RulesPage() {
const [openSections, setOpenSections] = useState<Set<number>>(new Set());
const [search, setSearch] = useState('');
const filtered = useMemo(() => {
if (!search.trim()) return battleReferenceSections.map((s, i) => ({ ...s, originalIndex: i }));
const q = search.toLowerCase();
return battleReferenceSections
.map((s, i) => ({ ...s, originalIndex: i }))
.filter((s) => s.title.toLowerCase().includes(q) || s.content.toLowerCase().includes(q));
}, [search]);
const toggle = (index: number) => {
setOpenSections((prev) => {
const next = new Set(prev);
if (next.has(index)) {
next.delete(index);
} else {
next.add(index);
}
return next;
});
};
const expandAll = () => {
setOpenSections(new Set(filtered.map((s) => s.originalIndex)));
};
const collapseAll = () => {
setOpenSections(new Set());
};
return (
<>
<TopBar title="Ship Reference" />
<PageContainer>
<input
type="search"
className="rules-search"
placeholder="Search rules..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="rules-actions">
<button className="btn-secondary btn-sm" onClick={expandAll}>
Expand All
</button>
<button className="btn-secondary btn-sm" onClick={collapseAll}>
Collapse All
</button>
{search.trim() && (
<span className="rules-count">{filtered.length} result{filtered.length !== 1 ? 's' : ''}</span>
)}
</div>
<div className="rules-sections">
{filtered.map((section) => (
<details key={section.originalIndex} className="rules-section" open={openSections.has(section.originalIndex)}>
<summary className="rules-summary" onClick={(e) => { e.preventDefault(); toggle(section.originalIndex); }}>
{section.title}
</summary>
<div className="rules-content">
<Markdown
remarkPlugins={[remarkGfm]}
components={{
table: ({ children }) => (
<div className="table-wrap"><table>{children}</table></div>
),
}}
>{section.content}</Markdown>
</div>
</details>
))}
{filtered.length === 0 && (
<p className="empty-text" style={{ padding: 'var(--spacing-md) 0' }}>
No sections match "{search}".
</p>
)}
</div>
</PageContainer>
</>
);
}