Skip to content

(SP: 2) [Frontend] Refactor Q&A Tabs: Extract Logic & Align Layout#129

Merged
ViktorSvertoka merged 5 commits into
developfrom
tabs-qa
Jan 11, 2026
Merged

(SP: 2) [Frontend] Refactor Q&A Tabs: Extract Logic & Align Layout#129
ViktorSvertoka merged 5 commits into
developfrom
tabs-qa

Conversation

@ViktorSvertoka
Copy link
Copy Markdown
Member

@ViktorSvertoka ViktorSvertoka commented Jan 11, 2026

Summary

Extract Q&A tab state/fetch/debounce logic into useQaTabs
Consolidate Q&A data types and reuse in AccordionList
Match Q&A tabs styling to Quizzes and align page container width

Changes

Added useQaTabs.ts and types.ts
Updated AccordionList.tsx to use shared types
Replaced TabsSection.tsx with QaSection.tsx
Adjusted page.tsx container to max-w-7xl with header padding

Close #128

Summary by CodeRabbit

  • New Features

    • New Q&A interface with search (clearable), category tabs, loading states and pagination.
  • Improvements

    • Wider, responsive Q&A page layout with improved spacing.
    • Better i18n for placeholders and "no results"/"no questions" messages.
  • Refactor

    • Consolidated Q&A types and a shared state hook powering the Q&A UI; replaced the previous tab implementation.

✏️ Tip: You can customize this high-level summary in your review settings.

@netlify
Copy link
Copy Markdown

netlify Bot commented Jan 11, 2026

Deploy Preview for develop-devlovers ready!

Name Link
🔨 Latest commit 6354dfd
🔍 Latest deploy log https://app.netlify.com/projects/develop-devlovers/deploys/6964207541c83b0008d64f7f
😎 Deploy Preview https://deploy-preview-129--develop-devlovers.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 11, 2026

Warning

Rate limit exceeded

@ViktorSvertoka has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 57 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between f44bf20 and 6354dfd.

📒 Files selected for processing (1)
  • frontend/components/q&a/useQaTabs.ts
📝 Walkthrough

Walkthrough

Extracts Q&A tab/search/pagination logic into a dedicated hook, centralizes Q&A types, replaces the monolithic TabsSection with QaSection, updates the Q&A page container classes, and adapts AccordionList to consume the shared types. (33 words)

Changes

Cohort / File(s) Summary
Page & Layout
frontend/app/[locale]/q&a/page.tsx
Swap import from TabsSectionQaSection and update main container classes from max-w-3xl mx-auto py-10 to mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8.
New Section Component
frontend/components/q&a/QaSection.tsx
Add client-side QaSection rendering search, category tabs, loading states, AccordionList, and pagination; delegates state and fetching to useQaTabs.
Hook Extraction
frontend/components/q&a/useQaTabs.ts
New hook: initializes from locale/URL, debounced search (400ms), URL sync, paginated API fetches, handlers (handleCategoryChange, handlePageChange, clearSearch), and exposes QA state.
Shared Types
frontend/components/q&a/types.ts
New centralized Q&A types/constants: locales, category slugs, rich-text block types, AnswerBlock union, QuestionEntry/QuestionApiItem, and PaginatedResponse<T>.
Component Type Update
frontend/components/q&a/AccordionList.tsx
Remove local type declarations; import and use shared types from types.ts; add/adjust runtime type guard(s); rendering logic preserved.
Removed Monolith
frontend/components/q&a/TabsSection.tsx
Deleted previous monolithic TabsSection that contained state, debounce, data fetching, and URL synchronization logic.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant QaSection
  participant useQaTabs
  participant Router
  participant API
  participant AccordionList

  User->>QaSection: interact (search / change tab / paginate)
  QaSection->>useQaTabs: call handlers (setSearch / changeCategory / changePage)
  useQaTabs->>Router: replace URL (category, page, search)
  useQaTabs->>API: GET /api/questions/{category}?page=&limit=10&locale=&search=
  API-->>useQaTabs: return paginated response
  useQaTabs-->>QaSection: provide items, totalPages, isLoading
  QaSection->>AccordionList: render items
  QaSection->>User: update UI (tabs, results, pagination)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • AM1007

Poem

🐰
I hopped through lines with nimble paws,
Split tabs and tucked away the claws.
Types in rows and hooks that sing,
Pages wider — let the queries ring.
A little refactor, joys it brings. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: refactoring Q&A tabs logic extraction and layout alignment with max-w-7xl container adjustment.
Linked Issues check ✅ Passed All coding requirements from issue #128 are met: TabsSection replaced with QaSection, useQaTabs hook created with state/fetch/debounce logic, types.ts centralized, AccordionList updated to use shared types, and page.tsx container aligned to max-w-7xl with padding.
Out of Scope Changes check ✅ Passed All changes directly support the stated objectives; no unrelated modifications detected beyond refactoring TabsSection, extracting logic, centralizing types, and adjusting layout.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @frontend/components/q&a/useQaTabs.ts:
- Around line 100-104: The fetch in useQaTabs.ts assigns the response to res and
immediately calls res.json(); add a response status check: after receiving res,
if (!res.ok) await res.text() (or include res.status/res.statusText) and throw
or return a structured error so non-2xx responses aren’t blindly parsed as JSON;
update the caller (the code using currentPage, searchParam and expecting
PaginatedResponse<QuestionApiItem>) to handle the thrown error or error return
path accordingly.
- Line 38: pageFromUrl can be NaN if searchParams.get('page') is non-numeric;
validate and fallback to 1 by parsing and checking isFinite/Number.isNaN (e.g.,
parseInt or Number then test), so replace the direct Number(...) assignment for
pageFromUrl with logic that parses the value, verifies it's a positive integer,
and falls back to 1 before using it to initialize state in useQaTabs
(referencing pageFromUrl and searchParams).
🧹 Nitpick comments (5)
frontend/components/q&a/QaSection.tsx (4)

12-12: Naming inconsistency: file exports TabsSection but file is named QaSection.tsx.

The file is named QaSection.tsx but exports function TabsSection(). This creates confusion as the import in page.tsx uses QaSection (relying on the filename). Consider renaming the function to match the file name for clarity.

♻️ Suggested fix
-export default function TabsSection() {
+export default function QaSection() {

43-48: Add accessible label to the clear search button.

The clear button lacks an aria-label, which impacts accessibility for screen reader users.

♿ Suggested fix
         <button
           onClick={clearSearch}
           className="absolute right-3 top-1/2 -translate-y-1/2"
+          aria-label={t('clearSearch')}
         >
           <X className="h-5 w-5" />
         </button>

Note: You'll need to add the clearSearch translation key, or use a hardcoded label like aria-label="Clear search".


70-72: Spinner may be invisible due to missing border color.

The spinner only has border-b-2 without specifying a color. Depending on the theme, this could render as transparent or match the background.

🎨 Suggested fix
             <div className="flex justify-center py-12">
-              <div className="animate-spin h-8 w-8 border-b-2" />
+              <div className="animate-spin h-8 w-8 border-b-2 border-blue-600 rounded-full" />
             </div>

53-53: Consider addressing the root cause of !important overrides.

Using multiple !important modifiers (!bg-transparent !p-0 !h-auto !w-full) suggests the base TabsList styles conflict with this use case. This is a code smell that may cause maintenance issues.

Consider either:

  1. Adjusting the base TabsList component to accept variant props
  2. Using a more specific selector or custom component
frontend/components/q&a/types.ts (1)

1-1: 'use client' directive is unnecessary for a types-only file.

This file only contains TypeScript type definitions, interfaces, and constants. The 'use client' directive has no effect here since types are erased at compile time and don't execute in the browser. Removing it improves clarity.

♻️ Suggested fix
-'use client';
-
 import { categoryData } from '@/data/category';
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8953f4d and 9f1f66d.

📒 Files selected for processing (6)
  • frontend/app/[locale]/q&a/page.tsx
  • frontend/components/q&a/AccordionList.tsx
  • frontend/components/q&a/QaSection.tsx
  • frontend/components/q&a/TabsSection.tsx
  • frontend/components/q&a/types.ts
  • frontend/components/q&a/useQaTabs.ts
💤 Files with no reviewable changes (1)
  • frontend/components/q&a/TabsSection.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
frontend/components/q&a/QaSection.tsx (5)
frontend/components/q&a/useQaTabs.ts (1)
  • useQaTabs (30-171)
frontend/components/ui/tabs.tsx (4)
  • Tabs (66-66)
  • TabsList (66-66)
  • TabsTrigger (66-66)
  • TabsContent (66-66)
frontend/data/category.ts (1)
  • categoryData (11-22)
frontend/components/q&a/AccordionList.tsx (1)
  • AccordionList (237-255)
frontend/components/q&a/Pagination.tsx (1)
  • Pagination (11-122)
frontend/components/q&a/types.ts (1)
frontend/data/category.ts (1)
  • categoryData (11-22)
frontend/components/q&a/useQaTabs.ts (2)
frontend/data/category.ts (1)
  • categoryData (11-22)
frontend/components/q&a/types.ts (6)
  • Locale (7-7)
  • qaConstants (95-97)
  • CategorySlug (8-8)
  • QuestionEntry (70-75)
  • PaginatedResponse (87-93)
  • QuestionApiItem (77-85)
🔇 Additional comments (8)
frontend/components/q&a/AccordionList.tsx (2)

12-25: LGTM! Clean centralization of type imports.

The type imports from the centralized types.ts module are well-organized and comprehensive. This improves maintainability by having a single source of truth for Q&A-related types.


27-34: Type guard implementation is correct.

The isListItemBlock guard properly validates the object structure with appropriate null/type checks before accessing properties.

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

3-3: LGTM! Component swap and layout alignment look correct.

The switch from TabsSection to QaSection and the updated container classes (max-w-7xl with responsive padding) align with the PR objectives to match the Quizzes layout.

Also applies to: 21-23

frontend/components/q&a/useQaTabs.ts (2)

72-89: LGTM! Debounce implementation is well-structured.

The debounce logic correctly:

  • Skips the initial mount using mountedRef
  • Clears previous timeouts before setting new ones
  • Properly cleans up on unmount via the effect cleanup function

127-139: LGTM! Handlers are properly memoized.

The handleCategoryChange, handlePageChange, and clearSearch callbacks are correctly wrapped with useCallback and have appropriate dependency arrays.

Also applies to: 141-148, 150-155

frontend/components/q&a/types.ts (3)

5-8: LGTM! Locale and CategorySlug types are well-derived.

Deriving Locale from SUPPORTED_LOCALES and CategorySlug from categoryData ensures type safety while maintaining a single source of truth.


24-41: Well-designed recursive type structure for nested lists.

The circular type references (ListEntryListItemBlockListItemChildBulletListBlock/NumberedListBlockListEntry) correctly model nested list structures. TypeScript handles this pattern properly with type aliases.


70-93: LGTM! API and domain interfaces are clean and well-defined.

The separation between QuestionEntry (UI model) and QuestionApiItem (API response model) follows good practices. The generic PaginatedResponse<T> enables reuse for other paginated endpoints.

Comment thread frontend/components/q&amp;a/useQaTabs.ts Outdated
Comment thread frontend/components/q&amp;a/useQaTabs.ts
@ViktorSvertoka ViktorSvertoka self-assigned this Jan 11, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @frontend/components/q&a/useQaTabs.ts:
- Around line 103-105: The fetch call in useQaTabs is using the raw locale param
instead of the validated localeKey; change the URL construction so it uses
localeKey (not locale) when calling fetch(`/api/questions/...`) and ensure any
useEffect dependency array that watches locale is updated to watch localeKey
instead (update the dependency array referenced near the fetch logic so effects
run based on the validated localeKey).
🧹 Nitpick comments (3)
frontend/components/q&a/useQaTabs.ts (3)

58-73: Variable shadowing: params shadows outer scope.

The params variable on line 60 shadows the params from useParams() on line 33. Consider renaming to searchParams or urlParams for clarity.

♻️ Suggested fix
  const updateUrl = useCallback(
    (category: CategorySlug, page: number, search: string) => {
-     const params = new URLSearchParams();
+     const urlParams = new URLSearchParams();

-     if (category !== DEFAULT_CATEGORY) params.set('category', category);
-     if (page > 1) params.set('page', String(page));
-     if (search) params.set('search', search);
+     if (category !== DEFAULT_CATEGORY) urlParams.set('category', category);
+     if (page > 1) urlParams.set('page', String(page));
+     if (search) urlParams.set('search', search);

-     const queryString = params.toString();
+     const queryString = urlParams.toString();

      router.replace(`/q&a${queryString ? `?${queryString}` : ''}`, {
        scroll: false,
      });
    },
    [router]
  );

75-92: Debounce effect triggers unnecessarily on category change.

When handleCategoryChange is called, it directly updates URL and resets search. However, this effect also runs because active is in the dependency array, scheduling a redundant debounced URL update.

Consider excluding active from triggering this debounce logic, or adding a condition to skip when searchQuery is unchanged.

♻️ Suggested approach: track previous search to avoid redundant updates
+ const prevSearchRef = useRef(searchFromUrl);

  useEffect(() => {
    if (!mountedRef.current) {
      mountedRef.current = true;
      return;
    }

+   // Skip if search hasn't actually changed
+   if (searchQuery === prevSearchRef.current) {
+     return;
+   }
+   prevSearchRef.current = searchQuery;

    if (debounceRef.current) clearTimeout(debounceRef.current);

    debounceRef.current = setTimeout(() => {
      setDebouncedSearch(searchQuery);
      setCurrentPage(1);
      updateUrl(active, 1, searchQuery);
    }, DEBOUNCE_MS);

    return () => {
      if (debounceRef.current) clearTimeout(debounceRef.current);
    };
- }, [searchQuery, active, updateUrl]);
+ }, [searchQuery, updateUrl]);

94-132: Consider adding request cancellation to prevent race conditions.

Rapid category or page changes could result in stale responses overwriting fresh data if an older request completes after a newer one. Adding an AbortController would prevent this.

♻️ Suggested implementation with AbortController
  useEffect(() => {
+   const controller = new AbortController();
+
    async function load() {
      setIsLoading(true);

      try {
        const searchParam = debouncedSearch
          ? `&search=${encodeURIComponent(debouncedSearch)}`
          : '';

        const res = await fetch(
-         `/api/questions/${active}?page=${currentPage}&limit=10&locale=${locale}${searchParam}`
+         `/api/questions/${active}?page=${currentPage}&limit=10&locale=${localeKey}${searchParam}`,
+         { signal: controller.signal }
        );

        if (!res.ok) {
          throw new Error(`Failed to load questions: ${res.status}`);
        }

        const data: PaginatedResponse<QuestionApiItem> = await res.json();

        setItems(
          data.items.map(item => ({
            id: item.id,
            question: item.question,
            category: active,
            answerBlocks: item.answerBlocks,
          }))
        );
        setTotalPages(data.totalPages);
      } catch (error) {
+       if (error instanceof Error && error.name === 'AbortError') {
+         return;
+       }
        console.error('Failed to load questions:', error);
        setItems([]);
        setTotalPages(0);
      } finally {
        setIsLoading(false);
      }
    }

    load();
+
+   return () => controller.abort();
- }, [active, currentPage, debouncedSearch, locale]);
+ }, [active, currentPage, debouncedSearch, localeKey]);
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 79f0a68 and f23f831.

📒 Files selected for processing (1)
  • frontend/components/q&a/useQaTabs.ts
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/components/q&a/useQaTabs.ts (2)
frontend/data/category.ts (1)
  • categoryData (11-22)
frontend/components/q&a/types.ts (6)
  • Locale (7-7)
  • qaConstants (95-97)
  • CategorySlug (8-8)
  • QuestionEntry (70-75)
  • PaginatedResponse (87-93)
  • QuestionApiItem (77-85)
🔇 Additional comments (4)
frontend/components/q&a/useQaTabs.ts (4)

1-18: LGTM!

Clean imports and well-defined constants. The fallback for DEFAULT_CATEGORY is good defensive coding.


20-28: LGTM!

Good type guards with sensible fallback behavior for invalid inputs.


38-48: LGTM!

Robust URL parameter parsing with proper validation for page numbers and category slugs.


134-178: LGTM!

Event handlers are well-structured with proper validation and state management. The exposed API is clean and complete for consumer components.

Comment thread frontend/components/q&amp;a/useQaTabs.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @frontend/components/q&a/useQaTabs.ts:
- Around line 35-36: The code unsafely casts params.locale to string; update the
logic around locale and localeKey so you normalize params.locale when calling
resolveLocale: check if params.locale is undefined, if it's an array use the
first element (or choose an appropriate preference), and if still falsy fall
back to a default locale before passing to resolveLocale; adjust the variables
referenced (params.locale, locale, localeKey, resolveLocale) so resolveLocale
always receives a string.
- Around line 94-132: The effect's async load() can leave multiple fetches in
flight causing stale responses to overwrite state; create an AbortController
inside the useEffect, pass controller.signal to the fetch call invoked in
load(), and in the effect cleanup call controller.abort() so prior requests are
cancelled when dependencies change; update the catch block in load() to ignore
abort errors (check error.name === 'AbortError' or similar) and only
setItems/setTotalPages for non-abort failures to avoid writing stale data.
🧹 Nitpick comments (1)
frontend/components/q&a/useQaTabs.ts (1)

75-92: Potential redundant URL update when category changes.

When handleCategoryChange is called, it immediately calls updateUrl and also sets searchQuery to ''. If searchQuery was already '', React won't re-render, but if it wasn't, this effect will fire after 400ms and call updateUrl again with the same values.

Consider removing active from the dependency array since the effect's purpose is to debounce search input, not category changes. Category changes are already handled synchronously in handleCategoryChange.

Suggested adjustment
  }, [searchQuery, active, updateUrl]);
+ // Note: Consider if `active` dependency is needed here since category changes
+ // are handled synchronously in handleCategoryChange

Alternatively, add an early return if searchQuery hasn't changed from the previous value.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f23f831 and f44bf20.

📒 Files selected for processing (1)
  • frontend/components/q&a/useQaTabs.ts
🔇 Additional comments (4)
frontend/components/q&a/useQaTabs.ts (4)

1-28: Well-structured utilities and constants.

The helper functions resolveLocale and isCategorySlug provide proper type narrowing. The fallback pattern for DEFAULT_CATEGORY handles edge cases gracefully.


58-73: Clean URL synchronization logic.

The conditional parameter inclusion keeps URLs clean by omitting default values. Using router.replace with scroll: false is appropriate for state-driven updates.


134-162: Well-structured handlers with proper memoization.

The handlers correctly validate inputs, update state synchronously, and maintain URL consistency. Using useCallback with appropriate dependencies ensures stable references for consumers.


164-177: Clean and comprehensive hook API.

The return object provides all necessary state and handlers for the Q&A tabs feature. The API aligns well with the PR objectives of extracting tab logic into a reusable hook.

Comment thread frontend/components/q&amp;a/useQaTabs.ts Outdated
Comment thread frontend/components/q&amp;a/useQaTabs.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

(SP: 2) [Frontend] Refactor Q&A Tabs: Extract Logic & Align Layout

1 participant