NeonRyan
Add User Guide (#53)
5807365 unverified
Raw
History Blame Contribute Delete
4 kB
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import * as LucideIcons from 'lucide-react';
import { X, Search, ChevronRight, BookOpen } from 'lucide-react';
import { useAppConfig } from '../contexts/AppConfigContext';
import { userGuideTopics } from '../data/userGuide';
import '../styles/UserGuide.css';
const UserGuide = () => {
const { config, advisors } = useAppConfig();
const [isOpen, setIsOpen] = useState(false);
const [activeId, setActiveId] = useState(userGuideTopics[0].id);
const [search, setSearch] = useState('');
useEffect(() => {
const open = () => setIsOpen(true);
window.addEventListener('open-user-guide', open);
return () => window.removeEventListener('open-user-guide', open);
}, []);
useEffect(() => {
if (!isOpen) return;
const onKey = (e) => e.key === 'Escape' && setIsOpen(false);
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [isOpen]);
const appName = config?.app?.title || 'the app';
const advisorEntries = Object.values(advisors || {});
const advisorCount = advisorEntries.length;
const advisorList = advisorEntries
.map((a) => `- **${a.name}:** ${a.description || a.role || ''}`.trimEnd())
.join('\n');
const q = search.toLowerCase().trim();
const filteredTopics = q
? userGuideTopics.filter(t =>
t.title.toLowerCase().includes(q) || t.content.toLowerCase().includes(q))
: userGuideTopics;
const activeTopic = userGuideTopics.find(t => t.id === activeId) || userGuideTopics[0];
if (!isOpen) return null;
return ReactDOM.createPortal(
<div className="ug-overlay" onClick={(e) => e.target === e.currentTarget && setIsOpen(false)}>
<div className="ug-modal" role="dialog" aria-label="User Guide">
{/* Header */}
<div className="ug-header">
<div className="ug-title">
<span className="ug-title-icon">
<BookOpen size={18} />
</span>
<span>User Guide</span>
</div>
<button className="ug-close" onClick={() => setIsOpen(false)} aria-label="Close">
<X size={20} />
</button>
</div>
<div className="ug-body">
{/* Sidebar / TOC */}
<aside className="ug-sidebar">
<div className="ug-search">
<Search size={14} />
<input
type="text"
placeholder="Search the guide…"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<nav className="ug-toc">
{filteredTopics.length === 0 && (
<div className="ug-empty">No matches</div>
)}
{filteredTopics.map((t) => {
const TopicIcon = LucideIcons[t.icon] || BookOpen;
return (
<button
key={t.id}
className={`ug-toc-item ${t.id === activeId ? 'active' : ''}`}
onClick={() => setActiveId(t.id)}
>
<TopicIcon size={16} />
<span>{t.title}</span>
<ChevronRight size={14} className="ug-toc-arrow" />
</button>
);
})}
</nav>
</aside>
{/* Content */}
<main className="ug-content" key={activeTopic.id}>
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{activeTopic.content
.replace(/\{\{appName\}\}/g, appName)
.replace(/\{\{advisorCount\}\}/g, String(advisorCount))
.replace(/\{\{advisorList\}\}/g, advisorList)}
</ReactMarkdown>
</main>
</div>
</div>
</div>,
document.body
);
};
export default UserGuide;