Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions frontend/app/[locale]/q&a/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Suspense } from 'react';
import { getTranslations } from 'next-intl/server';
import TabsSection from '@/components/q&a/TabsSection';
import QaSection from '@/components/q&a/QaSection';

export async function generateMetadata({
params,
Expand All @@ -18,9 +18,9 @@ export async function generateMetadata({

export default function QAPage() {
return (
<main className="max-w-3xl mx-auto py-10">
<main className="mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8">
<Suspense fallback={<>...</>}>
<TabsSection />
<QaSection />
</Suspense>
</main>
);
Expand Down
83 changes: 15 additions & 68 deletions frontend/components/q&a/AccordionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,73 +9,20 @@ import {
} from '@/components/ui/accordion';

import CodeBlock from '@/components/q&a/CodeBlock';

type TextNode = {
text: string;
bold?: boolean;
italic?: boolean;
code?: boolean;
boldItalic?: boolean;
};

type CodeBlock = {
type: 'code';
language: string | null;
content: string;
};

type ListEntry = ListItemBlock | ListItemChild;

type BulletListBlock = {
type: 'bulletList';
children: ListEntry[];
};

type NumberedListBlock = {
type: 'numberedList';
children: ListEntry[];
};

type ListItemChild = TextNode | CodeBlock | BulletListBlock | NumberedListBlock;

type ListItemBlock = {
type: 'listItem';
children: ListItemChild[];
};

type ParagraphBlock = {
type: 'paragraph';
children: TextNode[];
};

type HeadingBlock = {
type: 'heading';
level: 3 | 4;
children: TextNode[];
};

type TableCell = TextNode[];

type TableBlock = {
type: 'table';
header: TableCell[];
rows: TableCell[][];
};

type AnswerBlock =
| ParagraphBlock
| HeadingBlock
| BulletListBlock
| NumberedListBlock
| CodeBlock
| TableBlock;

type QuestionEntry = {
id?: number | string;
question: string;
category: string;
answerBlocks: AnswerBlock[];
};
import type {
AnswerBlock,
BulletListBlock,
CodeBlock as CodeBlockEntry,
HeadingBlock,
ListEntry,
ListItemBlock,
ListItemChild,
NumberedListBlock,
ParagraphBlock,
QuestionEntry,
TableBlock,
TextNode,
} from '@/components/q&a/types';

function isListItemBlock(value: ListEntry): value is ListItemBlock {
return (
Expand Down Expand Up @@ -145,7 +92,7 @@ function renderTextNodes(nodes: TextNode[]): ReactNode {
return nodes.map((node, i) => renderTextNode(node, i));
}

function renderCodeBlock(block: CodeBlock, index: number): ReactNode {
function renderCodeBlock(block: CodeBlockEntry, index: number): ReactNode {
return (
<CodeBlock key={index} code={block.content} language={block.language} />
);
Expand Down
95 changes: 95 additions & 0 deletions frontend/components/q&a/QaSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use client';

import { useTranslations } from 'next-intl';
import { Search, X } from 'lucide-react';

import AccordionList from '@/components/q&a/AccordionList';
import { Pagination } from '@/components/q&a/Pagination';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { categoryData } from '@/data/category';
import { useQaTabs } from '@/components/q&a/useQaTabs';

export default function TabsSection() {
const t = useTranslations('qa');
const {
active,
currentPage,
debouncedSearch,
handleCategoryChange,
handlePageChange,
isLoading,
items,
localeKey,
searchQuery,
setSearchQuery,
totalPages,
clearSearch,
} = useQaTabs();

return (
<div className="w-full">
<div className="relative mb-6">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />

<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder={t('searchPlaceholder')}
className="w-full pl-10 pr-10 py-3 border rounded-lg"
/>

{searchQuery && (
<button
onClick={clearSearch}
className="absolute right-3 top-1/2 -translate-y-1/2"
>
<X className="h-5 w-5" />
</button>
)}
</div>

<Tabs value={active} onValueChange={handleCategoryChange}>
<TabsList className="!bg-transparent !p-0 !h-auto !w-full grid grid-cols-3 sm:grid-cols-5 lg:grid-cols-10 mb-6 gap-2">
{categoryData.map(category => (
<TabsTrigger
key={category.slug}
value={category.slug}
className="data-[state=active]:bg-blue-600 data-[state=active]:text-white bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg px-3 py-2 text-sm font-medium transition-colors"
>
{category.translations[localeKey] ??
category.translations.en ??
category.slug}
</TabsTrigger>
))}
</TabsList>

{categoryData.map(category => (
<TabsContent key={category.slug} value={category.slug}>
{isLoading ? (
<div className="flex justify-center py-12">
<div className="animate-spin h-8 w-8 border-b-2" />
</div>
) : items.length ? (
<AccordionList items={items} />
) : (
<p className="text-center py-12">
{debouncedSearch
? t('noResults', { query: debouncedSearch })
: t('noQuestions')}
</p>
)}
</TabsContent>
))}
</Tabs>

{!isLoading && totalPages > 1 && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
/>
)}
</div>
);
}
Loading