diff --git a/web/src/components/faq/FAQContent.tsx b/web/src/components/faq/FAQContent.tsx index 5a308ef1..b136a28e 100644 --- a/web/src/components/faq/FAQContent.tsx +++ b/web/src/components/faq/FAQContent.tsx @@ -1,19 +1,96 @@ -import { useEffect, useRef } from 'react' -import { t, type Language } from '../../i18n/translations' -import type { FAQCategory } from '../../data/faqData' -// RoadmapWidget: removed dynamic embedding; only show external links on demand +import { Fragment, useEffect, useRef } from 'react' +import { ExternalLink } from 'lucide-react' +import type { FAQBlock, FAQCategory } from '../../data/faqData' interface FAQContentProps { categories: FAQCategory[] - language: Language onActiveItemChange: (itemId: string) => void } -export function FAQContent({ - categories, - language, - onActiveItemChange, -}: FAQContentProps) { +/** Renders text with inline `code` spans (backtick syntax). */ +function InlineText({ text }: { text: string }) { + const parts = text.split(/(`[^`]+`)/g) + return ( + <> + {parts.map((part, i) => + part.startsWith('`') && part.endsWith('`') ? ( + + {part.slice(1, -1)} + + ) : ( + {part} + ) + )} + + ) +} + +function Block({ block }: { block: FAQBlock }) { + switch (block.type) { + case 'p': + return ( +

+ +

+ ) + case 'list': + return ( + + ) + case 'steps': + return ( +
    + {block.items.map((item, i) => ( +
  1. + + {i + 1} + + + + +
  2. + ))} +
+ ) + case 'note': + return ( +
+ +
+ ) + case 'links': + return ( +
+ {block.links.map((link) => ( + + {link.label} + + + ))} +
+ ) + } +} + +export function FAQContent({ categories, onActiveItemChange }: FAQContentProps) { const sectionRefs = useRef>(new Map()) useEffect(() => { @@ -22,419 +99,60 @@ export function FAQContent({ entries.forEach((entry) => { if (entry.isIntersecting) { const itemId = entry.target.getAttribute('data-item-id') - if (itemId) { - onActiveItemChange(itemId) - } + if (itemId) onActiveItemChange(itemId) } }) }, - { - rootMargin: '-100px 0px -80% 0px', - threshold: 0, - } + { rootMargin: '-100px 0px -80% 0px', threshold: 0 } ) - sectionRefs.current.forEach((ref) => { - if (ref) observer.observe(ref) - }) - + sectionRefs.current.forEach((ref) => observer.observe(ref)) return () => { - sectionRefs.current.forEach((ref) => { - if (ref) observer.unobserve(ref) - }) + sectionRefs.current.forEach((ref) => observer.unobserve(ref)) } - }, [onActiveItemChange]) + }, [onActiveItemChange, categories]) const setRef = (itemId: string, element: HTMLElement | null) => { - if (element) { - sectionRefs.current.set(itemId, element) - } else { - sectionRefs.current.delete(itemId) - } + if (element) sectionRefs.current.set(itemId, element) + else sectionRefs.current.delete(itemId) } return ( -
+
{categories.map((category) => ( -
- {/* Category Header */} -
- -

- {t(category.titleKey, language)} +
+ {/* category header — terminal small-caps strip */} +
+ +

+ {category.title}

+ + {category.items.length} {category.items.length === 1 ? 'entry' : 'entries'} +
- {/* FAQ Items */} -
+
{category.items.map((item) => (
setRef(item.id, el)} - className="scroll-mt-24" + className="scroll-mt-24 px-5 py-5 md:px-6" > - {/* Question */} -

- {t(item.questionKey, language)} +

+ {item.question}

- - {/* Answer */} -
- {item.id === 'github-projects-tasks' ? ( -
- -
    - {language === 'zh' ? ( - <> -
  1. - Open the links above and filter by labels (good - first issue / help wanted / frontend / backend). -
  2. -
  3. - Open the task and read the Description & - Acceptance Criteria. -
  4. -
  5. - Comment "assign me" or self-assign (if permitted). -
  6. -
  7. Fork the repository to your GitHub account.
  8. -
  9. - Sync your fork's dev with upstream: - - git remote add upstream - https://github.com/NoFxAiOS/nofx.git - -
    - git fetch upstream -
    - git checkout dev -
    - git rebase upstream/dev -
    - git push origin dev -
  10. -
  11. - Create a feature branch from your fork's{' '} - dev: - - git checkout -b feat/your-topic - -
  12. -
  13. - Push to your fork: - - git push origin feat/your-topic - -
  14. -
  15. - Open a PR: base NoFxAiOS/nofx:dev ← - compare{' '} - your-username/nofx:feat/your-topic. -
  16. -
  17. - In PR, reference the Issue (e.g.,{' '} - Closes #123) and - choose the proper PR template; rebase onto{' '} - upstream/dev as needed. -
  18. - - ) : ( - <> -
  19. - Open the links above and filter by labels (good - first issue / help wanted / frontend / backend). -
  20. -
  21. - Open the task and read the Description & - Acceptance Criteria. -
  22. -
  23. - Comment "assign me" or self-assign (if permitted). -
  24. -
  25. Fork the repository to your GitHub account.
  26. -
  27. - Sync your fork's dev with upstream: - - git remote add upstream - https://github.com/NoFxAiOS/nofx.git - -
    - git fetch upstream -
    - git checkout dev -
    - git rebase upstream/dev -
    - git push origin dev -
  28. -
  29. - Create a feature branch from your fork's{' '} - dev: - - git checkout -b feat/your-topic - -
  30. -
  31. - Push to your fork: - - git push origin feat/your-topic - -
  32. -
  33. - Open a PR: base NoFxAiOS/nofx:dev ← - compare{' '} - your-username/nofx:feat/your-topic. -
  34. -
  35. - In PR, reference the Issue (e.g.,{' '} - Closes #123) and - choose the proper PR template; rebase onto{' '} - upstream/dev as needed. -
  36. - - )} -
- -
- {language === 'zh' ? ( -
- Note:{' '} - Contribution incentives are available (e.g., cash - bounties, badges & shout-outs, priority - review/merge, beta access). Prefer tasks with - - bounty label - - , or file a - - Bounty Claim - - after completion. -
- ) : ( -
- Note:{' '} - Contribution incentives are available (e.g., cash - bounties, badges & shout-outs, priority - review/merge, beta access). Prefer tasks with - - bounty label - - , or file a - - Bounty Claim - - after completion. -
- )} -
-
- ) : item.id === 'contribute-pr-guidelines' ? ( -
-
- {language === 'zh' ? 'References:' : 'References:'}{' '} - - CONTRIBUTING.md - - {' | '} - - PR_TITLE_GUIDE.md - -
-
    - {language === 'zh' ? ( - <> -
  1. - After forking, branch from your fork's{' '} - dev; avoid direct commits to upstream{' '} - main. -
  2. -
  3. - Branch naming: feat/…, fix/…, docs/…; commit - messages follow Conventional Commits. -
  4. -
  5. - Run checks before PR: - - npm --prefix web run lint && npm --prefix web - run build - -
  6. -
  7. - For UI changes, attach screenshots or a short - video. -
  8. -
  9. - Choose the proper PR template - (frontend/backend/docs/general). -
  10. -
  11. - Link the Issue in PR (e.g.,{' '} - Closes #123) and - target NoFxAiOS/nofx:dev. -
  12. -
  13. - Keep rebasing onto upstream/dev, - ensure CI passes; prefer small and focused PRs. -
  14. - - ) : ( - <> -
  15. - After forking, branch from your fork's{' '} - dev; avoid direct commits to upstream{' '} - main. -
  16. -
  17. - Branch naming: feat/…, fix/…, docs/…; commit - messages follow Conventional Commits. -
  18. -
  19. - Run checks before PR: - - npm --prefix web run lint && npm --prefix web - run build - -
  20. -
  21. - For UI changes, attach screenshots or a short - video. -
  22. -
  23. - Choose the proper PR template - (frontend/backend/docs/general). -
  24. -
  25. - Link the Issue in PR (e.g.,{' '} - Closes #123) and - target NoFxAiOS/nofx:dev. -
  26. -
  27. - Keep rebasing onto upstream/dev, - ensure CI passes; prefer small and focused PRs. -
  28. - - )} -
- -
- {language === 'zh' ? ( -
- Note:{' '} - We offer contribution incentives (bounties, badges, - shout-outs, priority review/merge, beta access). - Look for tasks with - - bounty label - - , or submit a - - Bounty Claim template - - when ready. -
- ) : ( -
- Note:{' '} - We offer contribution incentives (bounties, badges, - shout-outs, priority review/merge, beta access). - Look for tasks with - - bounty label - - , or submit a - - Bounty Claim - - when ready. -
- )} -
-
- ) : ( -

{t(item.answerKey, language)}

- )} +
+ {item.blocks.map((block, i) => ( + + ))}
- - {/* Divider */} -
))}
diff --git a/web/src/components/faq/FAQLayout.tsx b/web/src/components/faq/FAQLayout.tsx index af104b67..bf3510b8 100644 --- a/web/src/components/faq/FAQLayout.tsx +++ b/web/src/components/faq/FAQLayout.tsx @@ -1,158 +1,121 @@ -import { useState, useMemo } from 'react' +import { useMemo, useState } from 'react' import { HelpCircle } from 'lucide-react' import { DeepVoidBackground } from '../common/DeepVoidBackground' -import { t, type Language } from '../../i18n/translations' import { FAQSearchBar } from './FAQSearchBar' import { FAQSidebar } from './FAQSidebar' import { FAQContent } from './FAQContent' -import { faqCategories } from '../../data/faqData' +import { faqCategories, faqItemSearchText } from '../../data/faqData' import type { FAQCategory } from '../../data/faqData' -interface FAQLayoutProps { - language: Language -} - -export function FAQLayout({ language }: FAQLayoutProps) { +export function FAQLayout() { const [searchTerm, setSearchTerm] = useState('') const [activeItemId, setActiveItemId] = useState(null) - // Filter categories based on search term const filteredCategories = useMemo(() => { - if (!searchTerm.trim()) { - return faqCategories - } + if (!searchTerm.trim()) return faqCategories const term = searchTerm.toLowerCase() const filtered: FAQCategory[] = [] - faqCategories.forEach((category) => { - const matchingItems = category.items.filter((item) => { - const question = t(item.questionKey, language).toLowerCase() - const answer = t(item.answerKey, language).toLowerCase() - return question.includes(term) || answer.includes(term) - }) - + const matchingItems = category.items.filter((item) => + faqItemSearchText(item).includes(term) + ) if (matchingItems.length > 0) { - filtered.push({ - ...category, - items: matchingItems, - }) + filtered.push({ ...category, items: matchingItems }) } }) - return filtered - }, [searchTerm, language]) + }, [searchTerm]) + + const totalItems = useMemo( + () => faqCategories.reduce((sum, category) => sum + category.items.length, 0), + [] + ) const handleItemClick = (_categoryId: string, itemId: string) => { const element = document.getElementById(itemId) - if (element) { - const offset = 100 - const elementPosition = element.getBoundingClientRect().top - const offsetPosition = elementPosition + window.pageYOffset - offset - - window.scrollTo({ - top: offsetPosition, - behavior: 'smooth', - }) - } + if (!element) return + const offset = 100 + const top = + element.getBoundingClientRect().top + window.pageYOffset - offset + window.scrollTo({ top, behavior: 'smooth' }) } return ( - -
- {/* Page Header */} -
-
-
- + +
+ {/* page header — same strip language as the other terminal pages */} +
+
+
+
+ +
+
+

+ FAQ +

+

+ {totalItems} answers · wallets · launch · trading · self-hosting +

+
+
+
+
-
-

- {t('faqTitle', language)} -

-

- {t('faqSubtitle', language)} -

- - {/* Search Bar */} -
-
- {/* Main Content */} + {/* content */}
- {/* Sidebar - Hidden on mobile, visible on desktop */} -
- {/* Contact Section */} -
-

- {t('faqStillHaveQuestions', language)} + {/* still stuck */} +
+

+ Still have questions?

-

- {t('faqContactUs', language)} +

+ Ask in the community or open an issue — both are answered by the + people building NOFX.

-
+
GitHub @@ -160,13 +123,9 @@ export function FAQLayout({ language }: FAQLayoutProps) { href="https://t.me/nofx_dev_community" target="_blank" rel="noopener noreferrer" - className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105" - style={{ - background: '#E0483B', - color: '#F1ECE2', - }} + className="rounded-lg bg-nofx-gold px-5 py-2.5 font-mono text-xs font-bold uppercase tracking-[0.12em] text-white hover:bg-nofx-accent" > - {t('community', language)} + Telegram community
diff --git a/web/src/components/faq/FAQSidebar.tsx b/web/src/components/faq/FAQSidebar.tsx index 3eebb5a8..93d13a98 100644 --- a/web/src/components/faq/FAQSidebar.tsx +++ b/web/src/components/faq/FAQSidebar.tsx @@ -1,52 +1,44 @@ -import { t, type Language } from '../../i18n/translations' import type { FAQCategory } from '../../data/faqData' interface FAQSidebarProps { categories: FAQCategory[] activeItemId: string | null - language: Language onItemClick: (categoryId: string, itemId: string) => void } export function FAQSidebar({ categories, activeItemId, - language, onItemClick, }: FAQSidebarProps) { return (