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.
164 lines
6.0 KiB
TypeScript
164 lines
6.0 KiB
TypeScript
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>
|
|
);
|
|
}
|