Files
WooNooW/admin-spa/src/routes/Help/DocContent.tsx
Dwindi Ramadhana 68c3423f50 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.
2026-01-04 11:43:32 +07:00

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>
);
}