diff --git a/content/pt/perguntas/faq-exemplo.md b/content/pt/perguntas/faq-exemplo.md index ce2c271..35c5ff0 100644 --- a/content/pt/perguntas/faq-exemplo.md +++ b/content/pt/perguntas/faq-exemplo.md @@ -2,6 +2,8 @@ title: Como funcionam as classes de peso? readTime: 5 min intendedFor: Atletas +author: João Silva +authorImage: https://via.placeholder.com/60 --- As classes de peso determinam em que categoria o atleta compete. Consulte as regras da IPF para a lista atualizada de classes. diff --git a/src/app/globals.css b/src/app/globals.css index 227b391..6e671c6 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -118,7 +118,19 @@ p { font-size: 1rem; line-height: 1.6; color: var(--foreground); } } .markdown-content h1, .markdown-content h2, -.markdown-content h3 { margin-top: 1.25rem; margin-bottom: 0.75rem; } +.markdown-content h3 { + margin-top: 1.25rem; + margin-bottom: 0.75rem; + scroll-margin-top: 80px; /* Account for header + subnav on mobile */ +} + +@media (min-width: 768px) { + .markdown-content h1, + .markdown-content h2, + .markdown-content h3 { + scroll-margin-top: 120px; /* More space on desktop */ + } +} .markdown-content p, .markdown-content ul, .markdown-content ol { margin-bottom: 0.75rem; } .markdown-content ul { padding-left: 1.25rem; list-style: disc; } .markdown-content ol { padding-left: 1.25rem; list-style: decimal; } diff --git a/src/app/pt/perguntas/[slug]/page.tsx b/src/app/pt/perguntas/[slug]/page.tsx index 298400f..ee8fcf9 100644 --- a/src/app/pt/perguntas/[slug]/page.tsx +++ b/src/app/pt/perguntas/[slug]/page.tsx @@ -1,32 +1,91 @@ import { notFound } from "next/navigation"; -import { readFile } from "node:fs/promises"; +import { readFile, readdir } from "node:fs/promises"; import { join } from "node:path"; import { parseMarkdownWithFrontmatter } from "@/lib/markdown"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; -type Params = { params: { slug: string } }; +type Params = { params: Promise<{ slug: string }> }; + +async function getAvailableSlugs(): Promise { + try { + const root = process.cwd(); + const dir = join(root, "content", "pt", "perguntas"); + const files = await readdir(dir); + return files + .filter((f) => f.endsWith(".md")) + .map((f) => f.replace(/\.md$/, "")); + } catch { + return []; + } +} + +export async function generateStaticParams() { + const slugs = await getAvailableSlugs(); + return slugs.map((slug) => ({ + slug, + })); +} export default async function FaqArticlePage({ params }: Params) { + const { slug } = await params; const root = process.cwd(); - const file = join(root, "content", "pt", "perguntas", `${params.slug}.md`); + const file = join(root, "content", "pt", "perguntas", `${slug}.md`); try { const raw = await readFile(file); const { content, frontmatter } = parseMarkdownWithFrontmatter(raw.toString()); return ( -
-
-

{String(frontmatter.title ?? "FAQ")}

-

- {frontmatter.readTime ? ( - Tempo de leitura: {String(frontmatter.readTime)} - ) : null} - {frontmatter.intendedFor ? ( - • Público: {String(frontmatter.intendedFor)} - ) : null} -

+
+
+

+ {String(frontmatter.title ?? "FAQ")} +

+ + {/* Metadata Grid */} +
+ {frontmatter.readTime && ( +
+

+ Tempo de Leitura +

+

{String(frontmatter.readTime)}

+
+ )} + {frontmatter.intendedFor && ( +
+

+ Quem? +

+

{String(frontmatter.intendedFor)}

+
+ )} +
+ + {/* Author Section */} + {frontmatter.author && ( +
+ {frontmatter.authorImage && ( + {String(frontmatter.author)} + )} +
+

+ Autor +

+

+ {String(frontmatter.author)} +

+
+
+ )}
- {content} + +
+ {content} +
); } catch (e) { diff --git a/src/app/pt/perguntas/page.tsx b/src/app/pt/perguntas/page.tsx index 1645764..b040bf2 100644 --- a/src/app/pt/perguntas/page.tsx +++ b/src/app/pt/perguntas/page.tsx @@ -1,15 +1,24 @@ import Link from "next/link"; -import { readFile } from "node:fs/promises"; +import { readFile, readdir } from "node:fs/promises"; import { join } from "node:path"; import matter from "gray-matter"; export default async function FaqIndexPage() { - // Simple server component that lists available FAQ markdown files + // Dynamically read all FAQ markdown files const root = process.cwd(); - const files = ["faq-exemplo.md"]; // In a real app, glob directory + const faqDir = join(root, "content", "pt", "perguntas"); + + let files: string[] = []; + try { + files = await readdir(faqDir); + files = files.filter((f) => f.endsWith(".md")).sort(); + } catch { + files = []; + } + const entries: { slug: string; title: string; readTime?: string; intendedFor?: string }[] = []; for (const f of files) { - const raw = await readFile(join(root, "content", "pt", "perguntas", f)); + const raw = await readFile(join(faqDir, f)); const { data } = matter(raw.toString()); entries.push({ slug: f.replace(/\.md$/, ""), diff --git a/src/components/markdown/TableOfContents.tsx b/src/components/markdown/TableOfContents.tsx index d123be8..614fd54 100644 --- a/src/components/markdown/TableOfContents.tsx +++ b/src/components/markdown/TableOfContents.tsx @@ -5,9 +5,21 @@ type Heading = { id: string; text: string; level: number }; export function TableOfContents({ headings }: { headings: Heading[] }) { const [activeId, setActiveId] = useState(null); + const [isOpen, setIsOpen] = useState(false); const observerRef = useRef(null); useEffect(() => { + // Disconnect old observer if it exists + if (observerRef.current) { + observerRef.current.disconnect(); + } + + // Only set up observer if we have headings + if (headings.length === 0) { + setActiveId(null); + return; + } + const visibleHeadings = new Map(); const callback: IntersectionObserverCallback = (entries) => { @@ -30,31 +42,76 @@ export function TableOfContents({ headings }: { headings: Heading[] }) { }; const observer = new IntersectionObserver(callback, { - rootMargin: "-40% 0px -55% 0px", + rootMargin: "-80px 0px -55% 0px", threshold: 0.1, }); observerRef.current = observer; + headings.forEach((h) => { const el = document.getElementById(h.id); if (el) observer.observe(el); }); + return () => observer.disconnect(); }, [headings]); + const handleClick = (e: React.MouseEvent, headingId: string) => { + e.preventDefault(); + const element = document.getElementById(headingId); + if (element) { + element.scrollIntoView({ behavior: "smooth", block: "start" }); + // Update URL without causing a page reload + window.history.pushState(null, "", `#${headingId}`); + // Close mobile menu after click + setIsOpen(false); + } + }; + return ( - + <> + {/* Mobile Toggle Button */} + + + {/* Desktop Sidebar + Mobile Collapsible */} + + ); }