diff --git a/telcoinwiki-react/src/App.tsx b/telcoinwiki-react/src/App.tsx index 8618e07..3082e57 100644 --- a/telcoinwiki-react/src/App.tsx +++ b/telcoinwiki-react/src/App.tsx @@ -1,38 +1,40 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import styles from './App.module.css' +import type { SidebarHeading } from './config/types' +import { AppLayout } from './components/layout/AppLayout' +import { NAV_ITEMS } from './config/navigation' +import { PAGE_META } from './config/pageMeta' +import { SEARCH_CONFIG } from './config/search' -function App() { - const [count, setCount] = useState(0) +const demoHeadings: SidebarHeading[] = [ + { id: 'welcome', text: 'Welcome' }, + { id: 'next-steps', text: 'Next steps' }, +] +function App() { return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- + +
+

Community Q&A for Telcoin

+

React layout shell

+

+ This demo renders the site header, sidebar, and breadcrumb trail from the shared + configuration so content authors only touch JSON-like data files. +

+
+ +
+

Next steps

- Edit src/App.tsx and save to test HMR + Replace the placeholder sections with real page content. Update navigation, breadcrumb, and + search behaviour exclusively through the configuration modules in src/config.

-
-

- Click on the Vite and React logos to learn more -

- + + ) } diff --git a/telcoinwiki-react/src/components/layout/AppLayout.tsx b/telcoinwiki-react/src/components/layout/AppLayout.tsx new file mode 100644 index 0000000..9abd28d --- /dev/null +++ b/telcoinwiki-react/src/components/layout/AppLayout.tsx @@ -0,0 +1,57 @@ +import { useMemo } from 'react' +import type { + NavItem, + PageMetaMap, + SearchConfig, + SidebarHeading, +} from '../../config/types' +import type { ReactNode } from 'react' +import { Breadcrumbs } from './Breadcrumbs' +import { Header } from './Header' +import { Sidebar } from './Sidebar' + +interface AppLayoutProps { + pageId: string + navItems: NavItem[] + pageMeta: PageMetaMap + searchConfig: SearchConfig + headings?: SidebarHeading[] + children: ReactNode +} + +export function AppLayout({ + pageId, + navItems, + pageMeta, + searchConfig, + headings = [], + children, +}: AppLayoutProps) { + const currentMeta = pageMeta[pageId] ?? pageMeta.home + const activeNavId = currentMeta?.navId ?? pageId + + const sidebarItems = useMemo( + () => + Object.entries(pageMeta) + .filter(([, meta]) => meta.sidebar) + .map(([id, meta]) => ({ id, label: meta.label, href: meta.url })), + [pageMeta], + ) + + return ( + <> + + Skip to content + +
+
+
+ +
+ + {children} +
+
+ + ) +} diff --git a/telcoinwiki-react/src/components/layout/Breadcrumbs.tsx b/telcoinwiki-react/src/components/layout/Breadcrumbs.tsx new file mode 100644 index 0000000..631112e --- /dev/null +++ b/telcoinwiki-react/src/components/layout/Breadcrumbs.tsx @@ -0,0 +1,49 @@ +import type { BreadcrumbNode, PageMeta, PageMetaMap } from '../../config/types' + +interface BreadcrumbsProps { + pageId: string + pageMeta: PageMetaMap +} + +function buildBreadcrumbsTrail(pageId: string, pageMeta: PageMetaMap): BreadcrumbNode[] { + const trail: BreadcrumbNode[] = [] + let pointer: string | null = pageId + + while (pointer) { + const node: PageMeta | undefined = pageMeta[pointer] + if (!node) break + trail.unshift({ id: pointer, label: node.label, url: node.url }) + pointer = node.parent + } + + if (!trail.length || trail[0].id !== 'home') { + const home = pageMeta.home + if (home) { + trail.unshift({ id: 'home', label: home.label, url: home.url }) + } + } + + return trail +} + +export function Breadcrumbs({ pageId, pageMeta }: BreadcrumbsProps) { + const trail = buildBreadcrumbsTrail(pageId, pageMeta) + + return ( + + ) +} diff --git a/telcoinwiki-react/src/components/layout/Header.tsx b/telcoinwiki-react/src/components/layout/Header.tsx new file mode 100644 index 0000000..55c2643 --- /dev/null +++ b/telcoinwiki-react/src/components/layout/Header.tsx @@ -0,0 +1,179 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import type { NavItem, SearchConfig } from '../../config/types' + +interface HeaderProps { + navItems: NavItem[] + activeNavId?: string | null + searchConfig: SearchConfig +} + +export function Header({ navItems, activeNavId, searchConfig }: HeaderProps) { + const [openMenuId, setOpenMenuId] = useState(null) + const [mobileNavOpen, setMobileNavOpen] = useState(false) + const navListRef = useRef(null) + const { dataUrl, faqUrl, maxResultsPerGroup } = searchConfig + + useEffect(() => { + function handleDocumentClick(event: MouseEvent) { + if (!openMenuId) return + if (!(event.target instanceof Node)) return + if (navListRef.current && !navListRef.current.contains(event.target)) { + setOpenMenuId(null) + } + } + + document.addEventListener('click', handleDocumentClick) + return () => document.removeEventListener('click', handleDocumentClick) + }, [openMenuId]) + + useEffect(() => { + function handleKeydown(event: KeyboardEvent) { + if (event.key === 'Escape') { + setOpenMenuId(null) + } + } + + if (openMenuId) { + document.addEventListener('keydown', handleKeydown) + return () => document.removeEventListener('keydown', handleKeydown) + } + + return undefined + }, [openMenuId]) + + const mobileItems = useMemo(() => navItems, [navItems]) + + function toggleMenu(itemId: string) { + setOpenMenuId((current) => (current === itemId ? null : itemId)) + } + + function closeMenus() { + setOpenMenuId(null) + } + + function toggleMobileNav() { + setMobileNavOpen((current) => !current) + } + + function handleMobileLinkClick() { + setMobileNavOpen(false) + } + + return ( +
+
+ + Telcoin Wiki logo + + + + + + +
+ + +
+
+ + +
+ ) +} diff --git a/telcoinwiki-react/src/components/layout/Sidebar.tsx b/telcoinwiki-react/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2c4694d --- /dev/null +++ b/telcoinwiki-react/src/components/layout/Sidebar.tsx @@ -0,0 +1,58 @@ +import type { SidebarHeading } from '../../config/types' + +interface SidebarItemProps { + id: string + label: string + href: string +} + +interface SidebarProps { + items: SidebarItemProps[] + activeId?: string | null + headings?: SidebarHeading[] + isOpen?: boolean +} + +export function Sidebar({ items, activeId, headings = [], isOpen = false }: SidebarProps) { + return ( + + ) +} diff --git a/telcoinwiki-react/src/config/README.md b/telcoinwiki-react/src/config/README.md new file mode 100644 index 0000000..c5097ca --- /dev/null +++ b/telcoinwiki-react/src/config/README.md @@ -0,0 +1,15 @@ +# Layout configuration guide + +The React layout consumes a trio of typed configuration modules. Editors can update site +structure by editing these data exports instead of touching component code. + +- `navigation.ts` exports `NAV_ITEMS`, mirroring the legacy `js/main.js` array. Each item keeps the + same `id`, `label`, `href`, and optional `menu` shape so existing docs map 1:1. +- `pageMeta.ts` exports `PAGE_META`, the breadcrumb tree used across the wiki. Entries now accept an + optional `sidebar: true` flag that replaces the hard-coded sidebar list from the static HTML. + Add/remove this flag to control the “Knowledge base” links. +- `search.ts` exports `SEARCH_CONFIG`, preserving the `dataUrl`, `faqUrl`, and `maxResultsPerGroup` + keys from the static bundle. + +Component props (`NavItem`, `PageMeta`, `SearchConfig`, etc.) are defined in `types.ts`. Update these +types if a field name changes so TypeScript guides you to adjust dependent components. diff --git a/telcoinwiki-react/src/config/navigation.ts b/telcoinwiki-react/src/config/navigation.ts new file mode 100644 index 0000000..319c5c6 --- /dev/null +++ b/telcoinwiki-react/src/config/navigation.ts @@ -0,0 +1,28 @@ +import type { NavItems } from './types' + +export const NAV_ITEMS: NavItems = [ + { id: 'start-here', label: 'Start Here', href: '/start-here.html', menu: null }, + { + id: 'faq', + label: 'FAQ', + href: '/#faq', + menu: [ + { label: 'Basics', href: '/#faq-basics' }, + { label: 'Network & MNOs', href: '/#faq-network' }, + { label: 'Bank & eUSD', href: '/#faq-bank' }, + { label: 'Using TEL & App', href: '/#faq-app' }, + ], + }, + { + id: 'deep-dive', + label: 'Deep-Dive', + href: '/deep-dive.html', + menu: [ + { label: 'Telcoin Network', href: '/deep-dive.html#deep-network' }, + { label: '$TEL Token', href: '/deep-dive.html#deep-token' }, + { label: 'TELx Liquidity Engine', href: '/deep-dive.html#deep-telx' }, + { label: 'Association & Governance', href: '/deep-dive.html#deep-governance' }, + { label: 'Telcoin Holdings', href: '/deep-dive.html#deep-holdings' }, + ], + }, +] diff --git a/telcoinwiki-react/src/config/pageMeta.ts b/telcoinwiki-react/src/config/pageMeta.ts new file mode 100644 index 0000000..a72f66e --- /dev/null +++ b/telcoinwiki-react/src/config/pageMeta.ts @@ -0,0 +1,106 @@ +import type { PageMetaMap } from './types' + +export const PAGE_META: PageMetaMap = { + home: { label: 'Home', url: '/index.html', parent: null, navId: null }, + 'start-here': { + label: 'Start Here', + url: '/start-here.html', + parent: 'home', + navId: 'start-here', + sidebar: true, + }, + faq: { label: 'FAQ', url: '/faq/', parent: 'home', navId: 'faq', sidebar: true }, + wallet: { + label: 'Telcoin Wallet', + url: '/wallet.html', + parent: 'home', + navId: 'wallet', + sidebar: true, + }, + 'digital-cash': { + label: 'Digital Cash', + url: '/digital-cash.html', + parent: 'home', + navId: 'digital-cash', + sidebar: true, + }, + remittances: { + label: 'Remittances', + url: '/remittances.html', + parent: 'home', + navId: 'remittances', + sidebar: true, + }, + 'tel-token': { + label: 'TEL Token', + url: '/tel-token.html', + parent: 'home', + navId: 'tel-token', + sidebar: true, + }, + network: { + label: 'Telcoin Network', + url: '/network.html', + parent: 'home', + navId: 'network', + sidebar: true, + }, + telx: { + label: 'TELx Liquidity', + url: '/telx.html', + parent: 'home', + navId: 'telx', + sidebar: true, + }, + governance: { + label: 'Governance & Association', + url: '/governance.html', + parent: 'home', + navId: 'governance', + sidebar: true, + }, + builders: { + label: 'Builders', + url: '/builders.html', + parent: 'home', + navId: 'builders', + sidebar: true, + }, + links: { + label: 'Official Links', + url: '/links.html', + parent: 'home', + navId: 'links', + sidebar: true, + }, + 'deep-dive': { + label: 'Deep Dive', + url: '/deep-dive.html', + parent: 'home', + navId: 'deep-dive', + }, + pools: { + label: 'TELx Pools Dashboard', + url: '/pools.html', + parent: 'builders', + navId: 'builders', + }, + portfolio: { + label: 'TELx Portfolio Explorer', + url: '/portfolio.html', + parent: 'builders', + navId: 'builders', + }, + about: { + label: 'About this project', + url: '/about.html', + parent: 'home', + navId: null, + }, + '404': { + label: 'Page not found', + url: '/404.html', + parent: 'home', + navId: null, + }, +} diff --git a/telcoinwiki-react/src/config/search.ts b/telcoinwiki-react/src/config/search.ts new file mode 100644 index 0000000..ffb3ba5 --- /dev/null +++ b/telcoinwiki-react/src/config/search.ts @@ -0,0 +1,7 @@ +import type { SearchConfig } from './types' + +export const SEARCH_CONFIG: SearchConfig = { + dataUrl: '/data/search-index.json', + faqUrl: '/data/faq.json', + maxResultsPerGroup: 5, +} diff --git a/telcoinwiki-react/src/config/types.ts b/telcoinwiki-react/src/config/types.ts new file mode 100644 index 0000000..aee9c3c --- /dev/null +++ b/telcoinwiki-react/src/config/types.ts @@ -0,0 +1,40 @@ +export interface NavChild { + label: string + href: string +} + +export interface NavItem { + id: string + label: string + href: string + menu?: NavChild[] | null +} + +export type NavItems = NavItem[] + +export interface PageMeta { + label: string + url: string + parent: string | null + navId: string | null + sidebar?: boolean +} + +export type PageMetaMap = Record + +export interface SearchConfig { + dataUrl: string + faqUrl: string + maxResultsPerGroup: number +} + +export interface SidebarHeading { + id: string + text: string +} + +export interface BreadcrumbNode { + id: string + label: string + url: string +}