feat: Add in-app documentation system
Phase 1: Core Documentation
- Created docs/ folder with 8 markdown documentation files
- Getting Started, Installation, Troubleshooting, FAQ
- Configuration docs (Appearance, SPA Mode)
- Feature docs (Shop, Checkout)
- PHP registry with filter hook for addon extensibility
Phase 2: Documentation Viewer
- DocsController.php with REST API endpoints
- GET /woonoow/v1/docs - List all docs (with addon hook)
- GET /woonoow/v1/docs/{slug} - Get document content
- Admin SPA /help route with sidebar navigation
- Markdown rendering with react-markdown
- Added Help & Docs to More page for mobile access
Filter Hook: woonoow_docs_registry
Addons can register their own documentation sections.
This commit is contained in:
163
admin-spa/src/routes/Help/DocContent.tsx
Normal file
163
admin-spa/src/routes/Help/DocContent.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import type { DocContent as DocContentType } from './types';
|
||||
|
||||
interface DocContentProps {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export default function DocContent({ slug }: DocContentProps) {
|
||||
const [doc, setDoc] = useState<DocContentType | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDoc = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/wp-json/woonoow/v1/docs/${slug}`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setDoc(data.doc);
|
||||
} else {
|
||||
throw new Error(data.message || 'Failed to load document');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load document');
|
||||
setDoc(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDoc();
|
||||
}, [slug]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-10 w-3/4" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
<Skeleton className="h-32 w-full mt-6" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-5/6" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
{error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<article className="prose prose-slate dark:prose-invert max-w-none">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
// Custom heading with anchor links
|
||||
h1: ({ children }) => (
|
||||
<h1 className="text-3xl font-bold mb-6 pb-4 border-b">{children}</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4">{children}</h2>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<h3 className="text-xl font-medium mt-8 mb-3">{children}</h3>
|
||||
),
|
||||
// Styled tables
|
||||
table: ({ children }) => (
|
||||
<div className="overflow-x-auto my-6">
|
||||
<table className="min-w-full border-collapse border border-border rounded-lg">
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
th: ({ children }) => (
|
||||
<th className="border border-border bg-muted px-4 py-2 text-left font-semibold">
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
td: ({ children }) => (
|
||||
<td className="border border-border px-4 py-2">{children}</td>
|
||||
),
|
||||
// Styled code blocks
|
||||
code: ({ className, children }) => {
|
||||
const isInline = !className;
|
||||
if (isInline) {
|
||||
return (
|
||||
<code className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<code className={className}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
pre: ({ children }) => (
|
||||
<pre className="bg-muted p-4 rounded-lg overflow-x-auto my-4">
|
||||
{children}
|
||||
</pre>
|
||||
),
|
||||
// Styled blockquotes for notes
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="border-l-4 border-primary bg-primary/5 pl-4 py-2 my-4 italic">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
// Links
|
||||
a: ({ href, children }) => (
|
||||
<a
|
||||
href={href}
|
||||
className="text-primary hover:underline"
|
||||
target={href?.startsWith('http') ? '_blank' : undefined}
|
||||
rel={href?.startsWith('http') ? 'noopener noreferrer' : undefined}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
// Lists
|
||||
ul: ({ children }) => (
|
||||
<ul className="list-disc pl-6 my-4 space-y-2">{children}</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol className="list-decimal pl-6 my-4 space-y-2">{children}</ol>
|
||||
),
|
||||
// Horizontal rule
|
||||
hr: () => <hr className="my-8 border-border" />,
|
||||
}}
|
||||
>
|
||||
{doc.content}
|
||||
</ReactMarkdown>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user