mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-10-30 14:19:50 +00:00
feat: add KurdMedia, Help pages + fix Docs + wire routes + i18n (6 langs)
- Add KurdMediaPage (/social/kurdmedia): DKS media channels + social platform links - Add HelpPage (/help): coming soon with planned features + WhatsKURD shortcut - Rewrite Docs.tsx (/docs): clean documentation overview with section cards + docs.pezkuwichain.io button - Wire new routes in App.tsx: /social/kurdmedia, /help - Update MobileHomeLayout: kurdMedia → /social/kurdmedia, help → /help (fix crash) - Add i18n keys to all 6 locales: kurdMedia.*, help.*, docs.section.*, messaging.palletNotReady
This commit is contained in:
1 parent
eaccf65beb
commit
c56e021a6b
11 files changed
+591
-298
No files matched your search
+143
-295
@@ -1,311 +1,159 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import { ExternalLink } from 'lucide-react';
|
||||
import Layout from '@/components/Layout';
|
||||
import { marked } from 'marked';
|
||||
import { ChevronRight, Book, ExternalLink } from 'lucide-react';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
// SDK Embedded View - shown inline in the content area (window in window style)
|
||||
const SDKEmbeddedView: React.FC = () => {
|
||||
const sdkUrl = '/sdk_docs/pezkuwi_sdk_docs/index.html';
|
||||
const DOCS_URL = 'https://docs.pezkuwichain.io';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full min-h-[600px]">
|
||||
{/* SDK Panel Header */}
|
||||
<div className="flex items-center gap-3 px-4 py-3 bg-gray-800 rounded-t-lg border border-gray-700 border-b-0">
|
||||
<img
|
||||
src="/pezkuwi_icon.png"
|
||||
alt="Pezkuwi"
|
||||
className="w-8 h-8 rounded"
|
||||
/>
|
||||
<div>
|
||||
<h3 className="text-white font-semibold">pezkuwi_sdk_docs</h3>
|
||||
<span className="text-gray-400 text-xs">0.0.1</span>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<a
|
||||
href={sdkUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 hover:bg-gray-700 rounded-md transition-colors text-gray-400 hover:text-white"
|
||||
title="Open in new tab"
|
||||
>
|
||||
<ExternalLink size={16} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/* SDK Docs iframe */}
|
||||
<div className="flex-1 border border-gray-700 rounded-b-lg overflow-hidden bg-white">
|
||||
<iframe
|
||||
src={sdkUrl}
|
||||
title="Pezkuwi SDK Documentation"
|
||||
className="w-full h-full border-0"
|
||||
style={{ minHeight: '550px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SidebarNav: React.FC<{ structure: object, onLinkClick: () => void, onSDKClick: () => void }> = ({ structure, onLinkClick, onSDKClick }) => {
|
||||
const [openCategories, setOpenCategories] = useState<string[]>(['Getting Started', 'SDK Reference', 'General Docs', 'Contributor Guide']);
|
||||
|
||||
const toggleCategory = (category: string) => {
|
||||
setOpenCategories(prev =>
|
||||
prev.includes(category)
|
||||
? prev.filter(c => c !== category)
|
||||
: [...prev, category]
|
||||
);
|
||||
};
|
||||
|
||||
const renderNav = (struct: Record<string, unknown>) => {
|
||||
return Object.entries(struct).map(([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
// Check if it's the SDK docs special link
|
||||
const isSDKLink = value === 'sdk://open';
|
||||
|
||||
if (isSDKLink) {
|
||||
return (
|
||||
<li key={key}>
|
||||
<button
|
||||
onClick={() => {
|
||||
onSDKClick();
|
||||
}}
|
||||
className="w-full text-left block py-1 px-2 rounded-md hover:bg-gray-700 transition-colors font-bold text-green-400 hover:text-green-300 flex items-center gap-2"
|
||||
>
|
||||
{key}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
const path = value.replace(/\.(md|rs)$/, '');
|
||||
return (
|
||||
<li key={path}>
|
||||
<Link
|
||||
to={`/docs/${path}`}
|
||||
onClick={onLinkClick}
|
||||
className="block py-1 px-2 rounded-md hover:bg-gray-700 transition-colors text-gray-300 hover:text-white"
|
||||
>
|
||||
{key}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
} else {
|
||||
const isExpanded = openCategories.includes(key);
|
||||
return (
|
||||
<li key={key}>
|
||||
<div
|
||||
onClick={() => toggleCategory(key)}
|
||||
className="flex justify-between items-center cursor-pointer py-2 px-2 rounded-md hover:bg-gray-700"
|
||||
>
|
||||
<span className="font-semibold text-white">{key}</span>
|
||||
<ChevronRight size={16} className={`transform transition-transform text-gray-400 ${isExpanded ? 'rotate-90' : ''}`} />
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<ul className="pl-4 border-l border-gray-600 ml-2">
|
||||
{renderNav(value)}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return <nav><ul className="space-y-1">{renderNav(structure)}</ul></nav>;
|
||||
};
|
||||
const DOC_SECTIONS = [
|
||||
{
|
||||
icon: '📄',
|
||||
titleKey: 'docs.section.whitepaper',
|
||||
title: 'Whitepaper',
|
||||
descKey: 'docs.section.whitepaper.desc',
|
||||
desc: 'The foundational document describing the PezkuwiChain vision, architecture, and tokenomics.',
|
||||
path: '/whitepaper',
|
||||
color: 'bg-green-900/30 border-green-700/40',
|
||||
iconBg: 'bg-green-800',
|
||||
},
|
||||
{
|
||||
icon: '🏛️',
|
||||
titleKey: 'docs.section.architecture',
|
||||
title: 'Architecture',
|
||||
descKey: 'docs.section.architecture.desc',
|
||||
desc: 'Technical deep-dive into the blockchain architecture, consensus, pallets, and relay chain.',
|
||||
path: '/architecture',
|
||||
color: 'bg-blue-900/30 border-blue-700/40',
|
||||
iconBg: 'bg-blue-800',
|
||||
},
|
||||
{
|
||||
icon: '🚀',
|
||||
titleKey: 'docs.section.gettingStarted',
|
||||
title: 'Getting Started',
|
||||
descKey: 'docs.section.gettingStarted.desc',
|
||||
desc: 'Set up your wallet, get test tokens from the faucet, and make your first transaction.',
|
||||
path: '/getting-started',
|
||||
color: 'bg-yellow-900/30 border-yellow-700/40',
|
||||
iconBg: 'bg-yellow-800',
|
||||
},
|
||||
{
|
||||
icon: '⚙️',
|
||||
titleKey: 'docs.section.nodeSetup',
|
||||
title: 'Node Setup',
|
||||
descKey: 'docs.section.nodeSetup.desc',
|
||||
desc: 'Run a validator or full node on PezkuwiChain mainnet, testnet or local environment.',
|
||||
path: '/node-setup',
|
||||
color: 'bg-purple-900/30 border-purple-700/40',
|
||||
iconBg: 'bg-purple-800',
|
||||
},
|
||||
{
|
||||
icon: '🛠️',
|
||||
titleKey: 'docs.section.sdk',
|
||||
title: 'SDK Reference',
|
||||
descKey: 'docs.section.sdk.desc',
|
||||
desc: 'JavaScript/TypeScript SDK for building dApps on PezkuwiChain — API reference and examples.',
|
||||
path: '/sdk',
|
||||
color: 'bg-cyan-900/30 border-cyan-700/40',
|
||||
iconBg: 'bg-cyan-800',
|
||||
},
|
||||
{
|
||||
icon: '🤝',
|
||||
titleKey: 'docs.section.contributing',
|
||||
title: 'Contributor Guide',
|
||||
descKey: 'docs.section.contributing.desc',
|
||||
desc: 'How to contribute to PezkuwiChain — code, documentation, translations, and governance.',
|
||||
path: '/contributing',
|
||||
color: 'bg-red-900/30 border-red-700/40',
|
||||
iconBg: 'bg-red-800',
|
||||
},
|
||||
];
|
||||
|
||||
const QUICK_LINKS = [
|
||||
{ label: 'Mainnet RPC', value: 'wss://rpc.pezkuwichain.io' },
|
||||
{ label: 'Explorer', value: 'explorer.pezkuwichain.io', href: 'https://explorer.pezkuwichain.io' },
|
||||
{ label: 'Faucet', value: 'app.pezkuwichain.io/faucet', href: '/faucet' },
|
||||
{ label: 'GitHub', value: 'github.com/pezkuwichain', href: 'https://github.com/pezkuwichain' },
|
||||
];
|
||||
|
||||
const Docs: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { '*': splat } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [docStructure, setDocStructure] = useState<object | null>(null);
|
||||
const [content, setContent] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
const [showSDKLanding, setShowSDKLanding] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Fetch the documentation structure
|
||||
useEffect(() => {
|
||||
fetch('/docs-structure.json')
|
||||
.then(res => {
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to load documentation structure.');
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then(data => setDocStructure(data))
|
||||
.catch(e => setError(e.message));
|
||||
}, []);
|
||||
return (
|
||||
<Layout>
|
||||
<div className="max-w-4xl mx-auto px-4 py-10">
|
||||
|
||||
const filePath = useMemo(() => {
|
||||
// If no splat, and the structure is loaded, default to the introduction markdown
|
||||
if (!splat && docStructure) {
|
||||
const defaultEntry = docStructure['Introduction'];
|
||||
if (typeof defaultEntry === 'string') {
|
||||
return defaultEntry;
|
||||
}
|
||||
} else if (splat) {
|
||||
// Check if it's an SDK link which is an HTML file
|
||||
if (splat.startsWith('sdk_docs/') && splat.endsWith('html')) {
|
||||
return splat; // Treat as direct path, no .md or .rs append
|
||||
}
|
||||
return `${splat}.md`; // For .md or .rs files
|
||||
}
|
||||
return null; // No file selected, no default provided yet
|
||||
}, [splat, docStructure]);
|
||||
|
||||
// If no splat and no default, avoid fetching content
|
||||
const shouldFetchContent = !!filePath && !filePath.startsWith('sdk_docs/'); // Do not fetch content if it's an external SDK link
|
||||
{/* Hero */}
|
||||
<div className="text-center mb-10">
|
||||
<div className="text-6xl mb-4">📖</div>
|
||||
<h1 className="text-3xl font-bold text-white mb-2">
|
||||
{t('docs.title', 'PezkuwiChain Documentation')}
|
||||
</h1>
|
||||
<p className="text-gray-400 text-lg">
|
||||
{t('docs.subtitle', 'Everything you need to build on the Kurdish national blockchain')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldFetchContent) {
|
||||
setContent(''); // Clear content if not fetching
|
||||
setError(null);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
{/* Quick Links */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-10">
|
||||
{QUICK_LINKS.map(link => (
|
||||
<a
|
||||
key={link.label}
|
||||
href={link.href ?? '#'}
|
||||
target={link.href?.startsWith('http') ? '_blank' : undefined}
|
||||
rel="noopener noreferrer"
|
||||
className="bg-gray-800 rounded-xl p-3 hover:bg-gray-700 transition-colors border border-gray-700"
|
||||
>
|
||||
<p className="text-xs text-gray-400 mb-1">{link.label}</p>
|
||||
<p className="text-xs text-green-400 font-mono truncate">{link.value}</p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
const fetchContent = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch(`/docs/${filePath}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Documentation file not found: ${filePath}`);
|
||||
}
|
||||
let text = await response.text();
|
||||
|
||||
// If the file is a Rust file, wrap it in a markdown code block
|
||||
if (filePath.endsWith('.rs')) {
|
||||
text = '```rust\n' + text + '\n```';
|
||||
}
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
renderer.image = (href, title, text) => {
|
||||
try {
|
||||
// The base URL for the markdown file itself
|
||||
const base = new URL(`/docs/${filePath}`, window.location.origin);
|
||||
// Resolve the image's relative path against the markdown file's path
|
||||
const imageUrl = new URL(href, base);
|
||||
// Return the final path part of the URL
|
||||
return `<img src="${imageUrl.pathname}" alt="${text}" title="${title || ''}" />`;
|
||||
} catch (e) {
|
||||
console.error("Error processing image URL:", e);
|
||||
// Fallback to the original href if URL construction fails
|
||||
return `<img src="${href}" alt="${text}" title="${title || ''}" />`;
|
||||
}
|
||||
};
|
||||
marked.setOptions({ renderer });
|
||||
{/* Documentation Sections */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-10">
|
||||
{DOC_SECTIONS.map(section => (
|
||||
<a
|
||||
key={section.path}
|
||||
href={`${DOCS_URL}${section.path}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`flex gap-4 p-4 rounded-2xl border ${section.color} hover:opacity-90 transition-opacity cursor-pointer`}
|
||||
>
|
||||
<div className={`w-12 h-12 ${section.iconBg} rounded-xl flex items-center justify-center text-2xl flex-shrink-0`}>
|
||||
{section.icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="font-bold text-white text-sm mb-1">
|
||||
{t(section.titleKey, section.title)}
|
||||
</h2>
|
||||
<p className="text-xs text-gray-400 leading-relaxed">
|
||||
{t(section.descKey, section.desc)}
|
||||
</p>
|
||||
</div>
|
||||
<ExternalLink size={14} className="text-gray-500 flex-shrink-0 mt-1" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
const parsed = await marked.parse(text);
|
||||
const sanitized = DOMPurify.sanitize(parsed);
|
||||
setContent(sanitized);
|
||||
{/* Full Docs Button */}
|
||||
<div className="text-center">
|
||||
<p className="text-gray-500 text-sm mb-4">
|
||||
{t('docs.fullDocsNote', 'For complete and up-to-date documentation, visit the official documentation portal.')}
|
||||
</p>
|
||||
<a
|
||||
href={DOCS_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-green-600 hover:bg-green-500 text-white font-semibold rounded-xl transition-colors text-sm"
|
||||
>
|
||||
<ExternalLink size={16} />
|
||||
{t('docs.visitFullDocs', 'Visit docs.pezkuwichain.io')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
} catch (e: unknown) {
|
||||
setError(e instanceof Error ? e.message : 'Unknown error');
|
||||
setContent('');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchContent();
|
||||
}, [filePath, shouldFetchContent]); // Dependency array
|
||||
|
||||
// Check if we're on SDK route
|
||||
const isSDKRoute = splat === 'sdk';
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="flex h-full overflow-hidden">
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
className={`fixed lg:static top-16 left-0 h-full lg:h-auto z-30 w-64 bg-gray-800 lg:bg-transparent lg:w-1/4 lg:pr-8 py-4 transition-transform transform ${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'} lg:translate-x-0`}
|
||||
>
|
||||
<div className="px-4">
|
||||
{docStructure ? (
|
||||
<SidebarNav
|
||||
structure={docStructure}
|
||||
onLinkClick={() => {
|
||||
setIsSidebarOpen(false);
|
||||
setShowSDKLanding(false); // Clear SDK landing when navigating to other docs
|
||||
}}
|
||||
onSDKClick={() => {
|
||||
setIsSidebarOpen(false);
|
||||
setShowSDKLanding(true); // Show SDK landing
|
||||
setContent(''); // Clear any markdown content
|
||||
navigate('/docs/sdk');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-gray-400">{t('docs.loadingNav')}</p>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Mobile Sidebar Toggle */}
|
||||
<button
|
||||
className="fixed bottom-4 right-4 lg:hidden w-12 h-12 bg-green-600 rounded-full z-40 flex items-center justify-center text-white shadow-lg"
|
||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
>
|
||||
<Book size={24} />
|
||||
</button>
|
||||
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="w-full lg:w-3/4 lg:pl-8 flex flex-col">
|
||||
<div className="prose prose-invert prose-headings:text-cyan-400 prose-a:text-blue-400 hover:prose-a:text-blue-300 prose-code:text-yellow-400 prose-pre:bg-gray-800 prose-pre:p-4 prose-pre:rounded-md max-w-none flex-1 min-h-0">
|
||||
{isLoading && <p className="text-gray-400">{t('docs.loading')}</p>}
|
||||
{error && <p className="text-red-400">{t('docs.error')} {error}</p>}
|
||||
|
||||
{/* SDK Embedded View - window in window style */}
|
||||
{(showSDKLanding || isSDKRoute) && (
|
||||
<SDKEmbeddedView />
|
||||
)}
|
||||
|
||||
{/* Regular Markdown Content */}
|
||||
{!isLoading && !error && content && !showSDKLanding && !isSDKRoute && (
|
||||
<div dangerouslySetInnerHTML={{ __html: content }} />
|
||||
)}
|
||||
|
||||
{/* Default Welcome */}
|
||||
{!isLoading && !error && !content && !splat && !showSDKLanding && (
|
||||
<div className="text-center py-12">
|
||||
<div className="mb-8">
|
||||
<div className="text-6xl mb-4">📖</div>
|
||||
<h1 className="text-3xl font-bold text-white mb-2">{t('docs.title')}</h1>
|
||||
<p className="text-lg text-gray-400">{t('docs.subtitle')}</p>
|
||||
</div>
|
||||
<p className="text-xl text-gray-400 mb-4">
|
||||
{t('docs.selectDoc')}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4 justify-center mt-8">
|
||||
<Link to="/docs/GENESIS_ENGINEERING_PLAN" className="px-4 py-2 bg-green-600 hover:bg-green-500 text-white rounded-lg transition-colors">
|
||||
{t('docs.introduction')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/docs/sdk"
|
||||
onClick={() => setShowSDKLanding(true)}
|
||||
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors"
|
||||
>
|
||||
{t('docs.sdkDocs')}
|
||||
</Link>
|
||||
<Link to="/docs/whitepaper/whitepaper" className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors">
|
||||
{t('docs.whitepaper')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Docs;
|
||||
Reference in new issue
Block a user