Skip to content

Commit 777219f

Browse files
Merge pull request #129 from DevLoversTeam/tabs-qa
(SP: 2) [Frontend] Refactor Q&A Tabs: Extract Logic & Align Layout
2 parents 8953f4d + 6354dfd commit 777219f

6 files changed

Lines changed: 395 additions & 272 deletions

File tree

frontend/app/[locale]/q&a/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Suspense } from 'react';
22
import { getTranslations } from 'next-intl/server';
3-
import TabsSection from '@/components/q&a/TabsSection';
3+
import QaSection from '@/components/q&a/QaSection';
44

55
export async function generateMetadata({
66
params,
@@ -18,9 +18,9 @@ export async function generateMetadata({
1818

1919
export default function QAPage() {
2020
return (
21-
<main className="max-w-3xl mx-auto py-10">
21+
<main className="mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8">
2222
<Suspense fallback={<>...</>}>
23-
<TabsSection />
23+
<QaSection />
2424
</Suspense>
2525
</main>
2626
);

frontend/components/q&a/AccordionList.tsx

Lines changed: 15 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -9,73 +9,20 @@ import {
99
} from '@/components/ui/accordion';
1010

1111
import CodeBlock from '@/components/q&a/CodeBlock';
12-
13-
type TextNode = {
14-
text: string;
15-
bold?: boolean;
16-
italic?: boolean;
17-
code?: boolean;
18-
boldItalic?: boolean;
19-
};
20-
21-
type CodeBlock = {
22-
type: 'code';
23-
language: string | null;
24-
content: string;
25-
};
26-
27-
type ListEntry = ListItemBlock | ListItemChild;
28-
29-
type BulletListBlock = {
30-
type: 'bulletList';
31-
children: ListEntry[];
32-
};
33-
34-
type NumberedListBlock = {
35-
type: 'numberedList';
36-
children: ListEntry[];
37-
};
38-
39-
type ListItemChild = TextNode | CodeBlock | BulletListBlock | NumberedListBlock;
40-
41-
type ListItemBlock = {
42-
type: 'listItem';
43-
children: ListItemChild[];
44-
};
45-
46-
type ParagraphBlock = {
47-
type: 'paragraph';
48-
children: TextNode[];
49-
};
50-
51-
type HeadingBlock = {
52-
type: 'heading';
53-
level: 3 | 4;
54-
children: TextNode[];
55-
};
56-
57-
type TableCell = TextNode[];
58-
59-
type TableBlock = {
60-
type: 'table';
61-
header: TableCell[];
62-
rows: TableCell[][];
63-
};
64-
65-
type AnswerBlock =
66-
| ParagraphBlock
67-
| HeadingBlock
68-
| BulletListBlock
69-
| NumberedListBlock
70-
| CodeBlock
71-
| TableBlock;
72-
73-
type QuestionEntry = {
74-
id?: number | string;
75-
question: string;
76-
category: string;
77-
answerBlocks: AnswerBlock[];
78-
};
12+
import type {
13+
AnswerBlock,
14+
BulletListBlock,
15+
CodeBlock as CodeBlockEntry,
16+
HeadingBlock,
17+
ListEntry,
18+
ListItemBlock,
19+
ListItemChild,
20+
NumberedListBlock,
21+
ParagraphBlock,
22+
QuestionEntry,
23+
TableBlock,
24+
TextNode,
25+
} from '@/components/q&a/types';
7926

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

148-
function renderCodeBlock(block: CodeBlock, index: number): ReactNode {
95+
function renderCodeBlock(block: CodeBlockEntry, index: number): ReactNode {
14996
return (
15097
<CodeBlock key={index} code={block.content} language={block.language} />
15198
);
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
'use client';
2+
3+
import { useTranslations } from 'next-intl';
4+
import { Search, X } from 'lucide-react';
5+
6+
import AccordionList from '@/components/q&a/AccordionList';
7+
import { Pagination } from '@/components/q&a/Pagination';
8+
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
9+
import { categoryData } from '@/data/category';
10+
import { useQaTabs } from '@/components/q&a/useQaTabs';
11+
12+
export default function TabsSection() {
13+
const t = useTranslations('qa');
14+
const {
15+
active,
16+
currentPage,
17+
debouncedSearch,
18+
handleCategoryChange,
19+
handlePageChange,
20+
isLoading,
21+
items,
22+
localeKey,
23+
searchQuery,
24+
setSearchQuery,
25+
totalPages,
26+
clearSearch,
27+
} = useQaTabs();
28+
29+
return (
30+
<div className="w-full">
31+
<div className="relative mb-6">
32+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
33+
34+
<input
35+
type="text"
36+
value={searchQuery}
37+
onChange={e => setSearchQuery(e.target.value)}
38+
placeholder={t('searchPlaceholder')}
39+
className="w-full pl-10 pr-10 py-3 border rounded-lg"
40+
/>
41+
42+
{searchQuery && (
43+
<button
44+
onClick={clearSearch}
45+
className="absolute right-3 top-1/2 -translate-y-1/2"
46+
>
47+
<X className="h-5 w-5" />
48+
</button>
49+
)}
50+
</div>
51+
52+
<Tabs value={active} onValueChange={handleCategoryChange}>
53+
<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">
54+
{categoryData.map(category => (
55+
<TabsTrigger
56+
key={category.slug}
57+
value={category.slug}
58+
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"
59+
>
60+
{category.translations[localeKey] ??
61+
category.translations.en ??
62+
category.slug}
63+
</TabsTrigger>
64+
))}
65+
</TabsList>
66+
67+
{categoryData.map(category => (
68+
<TabsContent key={category.slug} value={category.slug}>
69+
{isLoading ? (
70+
<div className="flex justify-center py-12">
71+
<div className="animate-spin h-8 w-8 border-b-2" />
72+
</div>
73+
) : items.length ? (
74+
<AccordionList items={items} />
75+
) : (
76+
<p className="text-center py-12">
77+
{debouncedSearch
78+
? t('noResults', { query: debouncedSearch })
79+
: t('noQuestions')}
80+
</p>
81+
)}
82+
</TabsContent>
83+
))}
84+
</Tabs>
85+
86+
{!isLoading && totalPages > 1 && (
87+
<Pagination
88+
currentPage={currentPage}
89+
totalPages={totalPages}
90+
onPageChange={handlePageChange}
91+
/>
92+
)}
93+
</div>
94+
);
95+
}

0 commit comments

Comments
 (0)