Skip to content

feat(user-app): redesign full application and migrate from Chakra UI to Shadcn UI + Tailwind CSS#80

Merged
Abubokkor98 merged 12 commits into
devfrom
feature/redesign-chakra-to-shadcn
Jun 1, 2026
Merged

feat(user-app): redesign full application and migrate from Chakra UI to Shadcn UI + Tailwind CSS#80
Abubokkor98 merged 12 commits into
devfrom
feature/redesign-chakra-to-shadcn

Conversation

@Abubokkor98

@Abubokkor98 Abubokkor98 commented May 29, 2026

Copy link
Copy Markdown
Owner

📝 Description

This PR is a comprehensive redesign and architectural migration of the client-facing application from Chakra UI to Shadcn UI & Tailwind CSS. It improves application bundle size, resolves hydration/SSR mismatches, enforces a modern dark-mode aesthetic with glassmorphism/rich visual effects, and cleans up the routing structure.


🚀 Key Changes

1. Core Architecture & Routing Reorganization

  • Route Group Restructuring: Organized all user-facing routes under Next.js App Router route groups:
    • (main)/ group: Main application sections (Dashboard, Vocabulary, Quiz, Review, Analytics, Profile, Word List) wrapped under a shared layout.
    • (auth)/ group: Authentication pages (Login, Register, Forgot Password, Reset Password) separated from main layouts.
  • Hydration Resolution: Completely removed the ChakraUIProvider from root layout files, resolving SSR-to-client hydration mismatches and improving page load times.
  • Routing & Interceptions Fix: Restructured parallel and intercepting routes (vocabulary/@modal/(.)[id]) to function seamlessly under the Next.js (main) route group.

2. Redesigned Features (Tailwind CSS & Shadcn)

  • Landing Page:
    • Redesigned Hero and CTA sections featuring a animated dot-field background overlay.
    • Implemented a Bento-style layout for product features.
    • Integrated interactive visual components for testimonials, statistics, and instructional steps.
  • Dashboard:
    • Modernized layout with grid stat cards, animated quick action indicators, and responsive skeletons.
  • Analytics Dashboard:
    • Integrated charts showing difficulty distribution and performance history.
    • Redesigned recent quiz attempt lists, tables, and loading states.
  • Vocabulary & Word Details Modal:
    • Completely refactored WordDetailsModal to use the Radix-based Shadcn Dialog.
    • Added primary theme backdrop-blur styling (custom glassmorphism style) for overlays.
    • Enhanced definitions visual hierarchy, part-of-speech coloring, and responsive scrolling behavior.
  • Quiz & Review Systems:
    • Flashcards redesign with flip animations and sleek scoring ratings.
    • Redesigned quiz screens (start, active question, results analytics).
  • Word Lists:
    • New custom statistics trackers and an optimized list deletion modal.
  • User Profile Management:
    • Refactored profile cards, skeletons, forms, and custom validation alerts.

3. Shared Design System & Component Library (libs/ui)

  • Introduced a standard set of Shadcn components at the UI workspace level:
    • Badge, Button, Card, Dialog, DropdownMenu, Input, Label, Popover, Progress, Select, Separator, Skeleton, Table, Toast, Avatar.
  • Pagination Component: Restored custom pagination aesthetics, including active state highlighting using the primary brand color, unified borders, and state-reflective hover events.
  • User Menu: Modernized profile popover menus utilizing Radix-based Dropdowns and Avatars.
  • Custom Utilities: Extracted style merging behaviors into a standard cn utility wrapper.

🎨 Design Enhancements

  • Color Scheme: Consistent application of a dark-mode theme utilizing a sleek gray/slate palette accented by the primary brand color.
  • Glassmorphism: Subtly applied backdrop blurs (backdrop-blur-sm, bg-black/70, bg-background/80) to dialogs and overlay menus.
  • Micro-interactions: Integrated hover transitions and slide animation frames using Framer Motion (for desktop navigation links) and Tailwind utilities.

🛠️ Verification

  • Validated server building with pnpm build:user successfully.
  • Resolved route interception exceptions (/vocabulary/(.)[id]).
  • Checked and tested scroll containment on deep/tall modals.

Summary by CodeRabbit

  • New Features

    • Many public guides and pages added (About, Contact, Feedback, Privacy, Terms, multiple IELTS guides).
    • New interactive landing pieces: hero visuals, CTA, testimonials, stats, animated features.
  • UI/UX Improvements

    • Global visual refresh: glassmorphism theme, animations, Three.js hero, canvas dot-field, refined responsive layouts.
    • Updated navigation, footer, dialogs, toasts, forms, cards, tables, avatars, and "save to list" flows.
    • Analytics: explicit 7-day performance trend shown.
  • Other

    • Images remote loading allowlist extended (images.unsplash.com).

- Add pnpm script to install latest shadcn components in user app
- Include config file for shadcn UI with aliases and tailwind setup
- Integrate Radix UI dependencies and other support packages
- Extend tsconfig paths for consistent imports from ui library
- Update next.config.js for remote image access from unsplash
- Add custom DNS server fallback setup in backend mongo config
- Enhance quiz analytics to include 7-day trend data for progress charts
- Refine global CSS with React Bits design tokens, glassmorphism styles,
  animations, gradients, and micro-interactions
- Define new Tailwind utilities and keyframes for reusable visual effects
- Create UI components and styles for testimonials, call-to-action,
  footer with responsive layouts and ambient glow effects
- Optimize route matching in user proxy module
- Add missing typings for three.js and other UI related packages
- Clean up package.json scripts and dependencies for improved dev experience
@vercel

vercel Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ielts-vocabs-admin-app Ready Ready Preview, Comment Jun 1, 2026 6:06am
ielts-vocabs-backend Ready Ready Preview, Comment Jun 1, 2026 6:06am
ielts-vocabs-user-app Ready Ready Preview, Comment Jun 1, 2026 6:06am

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Abubokkor98, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 23 minutes and 1 second. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, 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 include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1997a5f1-981a-41a4-bb47-79c966998124

📥 Commits

Reviewing files that changed from the base of the PR and between c8f86c5 and 5a002d0.

📒 Files selected for processing (8)
  • apps/admin/src/app/dashboard/layout.tsx
  • apps/admin/src/features/admins/components/DeleteAdminDialog.tsx
  • apps/admin/src/features/dashboard/components/MetricCard.tsx
  • apps/admin/src/features/dashboard/index.tsx
  • apps/admin/src/features/settings/index.tsx
  • apps/admin/src/features/users/components/BanUserDialog.tsx
  • apps/admin/src/features/users/components/UserDetailModal.tsx
  • apps/admin/src/features/vocabulary/components/DeleteWordDialog.tsx
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/redesign-chakra-to-shadcn

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (16)
apps/user/src/app/global.css-303-303 (1)

303-303: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stylelint errors will fail keyframes-name-pattern (kebab-case).

Stylelint flags every camelCase @keyframes name in this file (marqueeScroll, marqueeScrollRev, testScrollUp, testScrollDown, spinCW, spinCCW, cursorBlink, thinkPulse, heroTextFadeIn, footerHeartBeat). Rename to kebab-case and update the corresponding animation/animation-name references (e.g. .animate-marquee, .animate-test-scroll-up, .ielts-master-btn, .ln-footer-heart) so the references stay in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/app/global.css` at line 303, Stylelint is failing due to
camelCase `@keyframes` names; rename each keyframe to kebab-case (e.g.,
marqueeScroll -> marquee-scroll, marqueeScrollRev -> marquee-scroll-rev,
testScrollUp -> test-scroll-up, testScrollDown -> test-scroll-down, spinCW ->
spin-cw, spinCCW -> spin-ccw, cursorBlink -> cursor-blink, thinkPulse ->
think-pulse, heroTextFadeIn -> hero-text-fade-in, footerHeartBeat ->
footer-heart-beat) and update every corresponding animation/animation-name
reference and related class usages (for example update .animate-marquee,
.animate-test-scroll-up, .ielts-master-btn, .ln-footer-heart and any other
.animate-* or animation declarations) so names remain in sync across the CSS.
apps/user/src/app/global.css-623-623 (1)

623-623: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove quotes around the single-token "Geist" font name.

Stylelint's font-family-name-quotes flags "Geist" on these lines. Drop the quotes for the single-token name (keep quotes on multi-word "Geist Mono").

🧹 Example fix
-  font-family: "Geist", sans-serif;
+  font-family: Geist, sans-serif;

Also applies to: 935-935, 991-991, 1030-1030

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/app/global.css` at line 623, The font-family declarations use
quoted single-token font name "Geist" which triggers stylelint's
font-family-name-quotes rule; update each occurrence of font-family: "Geist",
sans-serif; to remove the quotes so it reads font-family: Geist, sans-serif;
while leaving multi-word names like "Geist Mono" quoted; search for the exact
token font-family: "Geist" in the stylesheet and replace those instances
accordingly.
apps/user/src/components/ReviewCard.tsx-57-77 (1)

57-77: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

pointer-events-none doesn't block keyboard navigation on the disabled state.

When !isDue, the Button is disabled and the Link gets pointer-events-none, but the anchor remains keyboard-focusable and still navigates to /review on Enter — bypassing the intended block. Render the Link only when actionable (or mark the anchor non-interactive) so mouse and keyboard behave consistently.

🛡️ Proposed fix
-          <Link href="/review" className={cn(!isDue && "pointer-events-none")}>
+          <Link
+            href="/review"
+            aria-disabled={!isDue}
+            tabIndex={isDue ? undefined : -1}
+            className={cn(!isDue && "pointer-events-none")}
+          >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/components/ReviewCard.tsx` around lines 57 - 77, The Link
around the Button in ReviewCard.tsx remains keyboard-focusable when !isDue so
Enter still navigates; update the component to only render the Link when isDue
is true (wrap the Button with <Link> when isDue) and otherwise render a
non-interactive element (e.g., a span or disabled button) with
aria-disabled="true" and tabIndex={-1} so both mouse and keyboard are blocked;
ensure this change targets the Link and Button usage and preserves the existing
className and disabled prop on Button.
apps/user/src/components/footer/footer.constants.ts-7-57 (1)

7-57: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix footer internal links that don’t map to any user-app routes

  • apps/user/src/components/footer/footer.constants.ts adds internal hrefs /about, /contact, /feedback, /terms, /privacy, and /guides/*, but apps/user/src/app/**/page.* contains no matching route segments for any of them (and apps/user/next.config.js only rewrites /api/*, with no redirects/rewrites for these paths), so these footer links will 404.
  • Add the missing app-router pages or update the footer hrefs to routes that already exist.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/components/footer/footer.constants.ts` around lines 7 - 57,
PLATFORM_LINKS, EXAM_GUIDES, FREE_RESOURCES and STUDY_TIPS in
footer.constants.ts contain internal hrefs (e.g. /about, /contact, /feedback,
/terms, /privacy and /guides/*) that currently 404; fix by either (A) adding
matching app-router pages for each route (create page components for /about,
/contact, /feedback, /terms, /privacy and the /guides/* segments) or (B) update
the hrefs in PLATFORM_LINKS and EXAM_GUIDES to point to existing routes in the
user app; edit the array entries in footer.constants.ts (PLATFORM_LINKS,
EXAM_GUIDES) to reflect the chosen approach so all internal links resolve.
apps/user/src/components/navbar/mobile-nav.tsx-130-140 (1)

130-140: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

aria-modal dialog has no focus management.

The panel is marked role="dialog" with aria-modal="true", but focus is never moved into it on open and isn't trapped, so keyboard/screen-reader users can still tab to the background content behind the backdrop. Escape-to-close is handled, but consider moving initial focus into the panel and containing tab order while open (e.g. a small focus-trap or inert on the rest of the page).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/components/navbar/mobile-nav.tsx` around lines 130 - 140, The
dialog rendered in the motion.div (role="dialog", aria-modal="true") needs focus
management: when opening the component that uses DROPDOWN_VARIANTS and
transitionConfig, programmatically move keyboard focus into the panel (e.g., to
the first interactive element or a dedicated container) and implement a focus
trap so Tab/Shift+Tab cannot move focus to background content; alternatively set
inert on the rest of the page while open. Wire this behavior to the component's
open/close lifecycle (mount/open -> set initial focus and enable trap or inert;
unmount/close -> restore focus to the trigger) and ensure Escape handling
already present still closes and restores focus.
apps/user/src/features/auth/components/forgot-password-form.tsx-42-50 (1)

42-50: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add autoComplete="email" to the email input.

Without an autocomplete hint, browsers and password managers can't reliably prefill the field on this recovery flow.

♿ Proposed fix
               <Input
                 id="email"
                 type="email"
                 value={email}
                 onChange={(e) => setEmail(e.target.value)}
                 placeholder="your@email.com"
                 required
+                autoComplete="email"
                 className="bg-background text-foreground border-input"
               />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/features/auth/components/forgot-password-form.tsx` around lines
42 - 50, The email <Input> field (id="email") is missing an autocomplete hint;
add the autoComplete="email" prop to the Input component used in the
forgot-password form (the element with id="email" and value={email}
onChange={...}) so browsers and password managers can reliably prefill the
recovery email field.
apps/user/src/features/auth/components/register-form.tsx-38-76 (1)

38-76: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add autoComplete hints to the registration inputs.

For sign-up flows, set autoComplete="name", autoComplete="email", and autoComplete="new-password" so browsers and password managers can offer to generate/save credentials.

♿ Proposed fix
               <Input
                 id="name"
                 type="text"
                 value={name}
                 onChange={(e) => setName(e.target.value)}
                 placeholder="John Doe"
                 required
+                autoComplete="name"
                 className="bg-background text-foreground border-input"
               />
               <Input
                 id="email"
                 type="email"
                 value={email}
                 onChange={(e) => setEmail(e.target.value)}
                 placeholder="your@email.com"
                 required
+                autoComplete="email"
                 className="bg-background text-foreground border-input"
               />
               <Input
                 id="password"
                 type="password"
                 value={password}
                 onChange={(e) => setPassword(e.target.value)}
                 placeholder="Min 6 characters"
                 required
+                autoComplete="new-password"
                 className="bg-background text-foreground border-input"
               />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/features/auth/components/register-form.tsx` around lines 38 -
76, Add HTML autocomplete hints to the registration inputs so browsers/password
managers can assist: update the Input with id "name" (where value={name} and
onChange={setName}) to include autoComplete="name", the Input with id "email"
(value={email}) to include autoComplete="email", and the Input with id
"password" (value={password}) to include autoComplete="new-password"; modify
these three Input components in register-form.tsx (the name/email/password Input
elements) accordingly.
apps/user/src/features/auth/components/login-form.tsx-37-60 (1)

37-60: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add autoComplete hints to the login inputs.

Login forms benefit significantly from autofill: email should use autoComplete="email" and the password autoComplete="current-password" so browsers/password managers prefill and offer to save credentials.

♿ Proposed fix
               <Input
                 id="email"
                 type="email"
                 value={email}
                 onChange={(e) => setEmail(e.target.value)}
                 placeholder="your@email.com"
                 required
+                autoComplete="email"
                 className="bg-background text-foreground border-input"
               />
               <Input
                 id="password"
                 type="password"
                 value={password}
                 onChange={(e) => setPassword(e.target.value)}
                 placeholder="••••••••"
                 required
+                autoComplete="current-password"
                 className="bg-background text-foreground border-input"
               />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/features/auth/components/login-form.tsx` around lines 37 - 60,
Add browser autofill hints to the login inputs by setting autoComplete="email"
on the Input with id="email" and autoComplete="current-password" on the Input
with id="password" (the components that use setEmail and setPassword). Update
those two Input props in the LoginForm component so password managers and
browsers can prefill and save credentials.
apps/user/src/features/landing/components/hero-section.tsx-81-89 (1)

81-89: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Cancel in-flight speech on unmount.

triggerAudioWave starts speechSynthesis.speak(...) but nothing cancels it when HeroSection unmounts (e.g. user navigates away mid-pronunciation), so the audio keeps playing after the component is gone.

🛡️ Add a cleanup effect
   React.useEffect(() => {
     const onClickOutside = (e: MouseEvent) => {
       if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
         setIsDropdownOpen(false);
       }
     };
     document.addEventListener('pointerdown', onClickOutside);
     return () => document.removeEventListener('pointerdown', onClickOutside);
   }, []);
+
+  React.useEffect(() => {
+    return () => {
+      if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
+        window.speechSynthesis.cancel();
+      }
+    };
+  }, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/features/landing/components/hero-section.tsx` around lines 81 -
89, The component starts speech with triggerAudioWave calling
speechSynthesis.speak(...) but never cancels it on unmount; update HeroSection
to cancel any in-flight speech in a cleanup effect by calling
speechSynthesis.cancel() (or cancel specific utterance if you store it) inside a
useEffect cleanup so ongoing audio is stopped when the component unmounts;
locate triggerAudioWave and add either a stored SpeechSynthesisUtterance
reference or a simple speechSynthesis.cancel() in the component's cleanup to
ensure speech stops on unmount.
apps/user/src/features/landing/components/testimonials.tsx-192-198 (1)

192-198: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Hide duplicated testimonial sets from assistive tech.

SET_PREFIXES duplicates the same testimonials three times purely for the seamless scroll animation. Screen readers will announce every testimonial three times. Mark all but the first set aria-hidden so the duplicates are visual-only.

♿ Proposed fix
-        {SET_PREFIXES.map((prefix) => (
-          <li className="ln-test-col-set" key={prefix}>
+        {SET_PREFIXES.map((prefix, setIndex) => (
+          <li className="ln-test-col-set" key={prefix} aria-hidden={setIndex > 0}>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/features/landing/components/testimonials.tsx` around lines 192
- 198, The duplicated testimonial sets created by SET_PREFIXES are announced
multiple times by screen readers; when mapping SET_PREFIXES, add aria-hidden to
the outer list item (the element with class "ln-test-col-set") for every set
except the first (use the map index to detect i > 0) so duplicates are
visual-only; keep the existing keys (e.g. `${prefix}-${testimonial.handle}`) and
leave TestimonialCard unchanged.
apps/user/src/features/review/components/flashcard.tsx-30-40 (1)

30-40: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a keyboard handler to the role="button" card.

The card is exposed as an interactive button (role="button", tabIndex={0}) but only handles onClick. A keyboard user who focuses the card and presses Enter/Space gets no response from the element itself, so flip-by-keyboard relies entirely on the global shortcut. Wire an onKeyDown to activate the flip when focused.

♿ Proposed fix
     <Card
       onClick={() => !isFlipped && onFlip()}
       role="button"
       tabIndex={0}
+      onKeyDown={(e) => {
+        if (!isFlipped && (e.key === 'Enter' || e.key === ' ')) {
+          e.preventDefault();
+          onFlip();
+        }
+      }}
       className={cn(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/features/review/components/flashcard.tsx` around lines 30 - 40,
The Card element (the component with role="button", tabIndex={0}) only has
onClick; add an onKeyDown handler on that same Card which checks for Enter
(e.g., key === "Enter") and Space (e.g., key === " " or key === "Spacebar") and
triggers onFlip() when !isFlipped, using event.preventDefault() for Space to
avoid page scroll; ensure the handler mirrors the onClick guard (!isFlipped &&
onFlip()) so keyboard focus behaves identically to mouse clicks and does nothing
when isFlipped is true.
apps/user/src/features/vocabulary/components/vocabulary-filters.tsx-106-114 (1)

106-114: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add an accessible name to the icon-only clear buttons.

Both clear buttons render only the <X> icon, so they expose no accessible name to assistive technology. Add an aria-label (the lucide icon is decorative and should be aria-hidden).

♿ Proposed fix
           {wordSearch && (
             <button
               type="button"
               onClick={() => onWordSearchChange('')}
+              aria-label="Clear vocabulary search"
               className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
             >
-              <X className="h-4 w-4" />
+              <X className="h-4 w-4" aria-hidden="true" />
             </button>
           )}

Apply the same change to the topic search clear button (lines 127-133) with an appropriate label such as "Clear topic search".

Also applies to: 126-134

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/features/vocabulary/components/vocabulary-filters.tsx` around
lines 106 - 114, The clear buttons that render only the <X> icon lack accessible
names; update the button for wordSearch (the button that calls
onWordSearchChange('') and displays <X>) and the topic search clear button (the
button that calls onTopicSearchChange('') / references topicSearch) to include a
descriptive aria-label (e.g., "Clear word search" and "Clear topic search"), and
mark the Lucide <X> icon itself as decorative with aria-hidden="true" so
assistive tech reads the button label instead of the icon.
apps/user/src/features/word-list/components/list-card.tsx-212-223 (1)

212-223: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Drop the redundant onClick on the popover trigger.

The Popover is already controlled via open/onOpenChange, and Radix's PopoverTrigger toggles open state on click. The extra onClick={() => setIsMenuOpen(true)} forces true on every trigger click, which can fight Radix's toggle and prevent the menu from closing when the trigger is clicked while open.

🐛 Proposed fix
                 <Button
                   variant="ghost"
                   size="icon"
                   className="h-7 w-7 text-muted-foreground/60 hover:text-foreground hover:bg-white/5 rounded-lg transition-colors"
                   aria-label="List actions"
-                  onClick={() => setIsMenuOpen(true)}
                 >
                   <MoreHorizontal size={14} />
                 </Button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/features/word-list/components/list-card.tsx` around lines 212 -
223, The Popover is controlled by the open/onOpenChange pair but the
PopoverTrigger's Button also has an explicit onClick={() =>
setIsMenuOpen(true)}, which forces isMenuOpen to true and prevents Radix from
toggling/closing; remove the onClick prop from the Button inside PopoverTrigger
so that Popover, PopoverTrigger, isMenuOpen and setIsMenuOpen manage open state
consistently.
apps/user/src/features/word-list/components/delete-confirmation-modal.tsx-40-92 (1)

40-92: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add dialog semantics and keyboard dismissal.

This hand-rolled modal is missing role="dialog"/aria-modal, an accessible label, Escape-to-close, and focus management. Since @ielts/ui already exposes a Radix Dialog (used elsewhere in this PR), prefer reusing it for built-in focus trap, Escape handling, and ARIA wiring. If keeping the custom version, add at minimum a role, label, and an Escape handler.

♿ Minimal fix for the custom modal
+  useEffect(() => {
+    if (!isOpen) return;
+    const onKey = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') onClose();
+    };
+    document.addEventListener('keydown', onKey);
+    return () => document.removeEventListener('keydown', onKey);
+  }, [isOpen, onClose]);
+
   return createPortal(
     <AnimatePresence>
       {isOpen && (
         <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
           {/* Overlay */}
           <motion.div
             initial={{ opacity: 0 }}
             animate={{ opacity: 1 }}
             exit={{ opacity: 0 }}
             className="fixed inset-0 bg-black/80 backdrop-blur-md"
             onClick={onClose}
           />
           {/* Dialog Card */}
           <motion.div
+            role="dialog"
+            aria-modal="true"
+            aria-labelledby="delete-collection-title"
             initial={{ opacity: 0, scale: 0.95, y: 15 }}
-            <h3 className="text-base font-bold text-foreground mb-2">Delete Collection</h3>
+            <h3 id="delete-collection-title" className="text-base font-bold text-foreground mb-2">Delete Collection</h3>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/features/word-list/components/delete-confirmation-modal.tsx`
around lines 40 - 92, The modal lacks ARIA dialog semantics, focus trapping,
Escape-to-close, and an accessible label; update delete-confirmation-modal.tsx
to use the existing Radix-based Dialog from `@ielts/ui` (instead of the
hand-rolled createPortal/AnimatePresence/motion block) so you get
role/aria-modal, focus management and Escape handling out of the box, wiring the
Dialog.Trigger/Content/Title to the existing onClose/onConfirm handlers and
using listName for the label; if you must keep the custom implementation, add
role="dialog" aria-modal="true" give the <h3> an id and set aria-labelledby to
that id on the dialog container, attach a keydown listener to the top-level
container to call onClose on Escape, and ensure focus is trapped/returned (set
initial focus to the Cancel or Delete Button and restore focus to the opener on
close) while preserving isOpen/isPending/onClose/onConfirm behavior.
apps/user/src/features/word-list/hooks/use-save-to-list.ts-58-60 (1)

58-60: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard handleCreateAndAdd against re-entry.

handleKeyDown invokes handleCreateAndAdd() on every Enter keypress, and the only guard is the empty-name check. Since isCreating is set inside the async body, rapid Enter presses (or Enter + clicking Create) can fire multiple createList requests and create duplicate lists. Add an early return when already in flight.

🛡️ Proposed guard
   const handleCreateAndAdd = async () => {
     const trimmedName = newListName.trim();
-    if (!trimmedName) return;
+    if (!trimmedName || isCreating) return;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/features/word-list/hooks/use-save-to-list.ts` around lines 58 -
60, handleCreateAndAdd can be re-entered by rapid Enter presses; add an early
return that checks the isCreating flag at the top of handleCreateAndAdd (e.g.,
if (isCreating) return) and set isCreating = true immediately before any await
(and reset it in a finally block) so createList cannot be invoked concurrently;
reference the handleCreateAndAdd function, the isCreating state, and the
createList call to locate where to add the guard and the early state toggle.
libs/ui/src/lib/WordDetailsModal.tsx-43-43 (1)

43-43: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Provide a proper description instead of suppressing aria-describedby.

Setting aria-describedby={undefined} explicitly removes the accessibility attribute that helps screen reader users understand the dialog's purpose. Consider either providing a proper description via DialogDescription or allowing Radix to use its default behavior.

♿ Suggested approach

Add a DialogDescription component after the header to describe the modal content:

<DialogContent className="..." aria-describedby="word-description">
  {/* ... header ... */}
  <DialogDescription id="word-description" className="sr-only">
    Detailed information about the word {word.word}
  </DialogDescription>
  {/* ... body ... */}
</DialogContent>

Or remove the prop entirely to let Radix handle it:

-<DialogContent className="..." aria-describedby={undefined}>
+<DialogContent className="...">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/ui/src/lib/WordDetailsModal.tsx` at line 43, The DialogContent currently
sets aria-describedby={undefined}, which removes an accessibility description;
update the modal to either remove the aria-describedby prop so Radix can handle
it, or add a proper DialogDescription element tied to DialogContent (e.g., give
DialogDescription an id like "word-description" and reference that id from
DialogContent) to describe the dialog content (include the specific word via
word.word and mark the description visually hidden with a sr-only class if
needed); adjust the DialogContent/ DialogDescription usage so screen readers
receive the descriptive text instead of undefined.
🧹 Nitpick comments (3)
apps/user/src/features/word-list/index.tsx (1)

46-46: ⚡ Quick win

Clarify useWordLists() auth gating on /my-lists

useWordLists defaults enabled = true, so apps/user/src/features/word-list/index.tsx:46 will always fetch /word-lists even when isAuthenticated is false (unlike use-save-to-list.ts, which passes useWordLists(isAuthenticated)). This isn’t likely to break logged-in users because axiosInstance attaches accessToken when present and auto-refreshes on 401, but it can still trigger an auth refresh + possible redirect when visiting /my-lists while logged out. Consider gating the query with hasHydrated/cookie-capable auth intent to avoid unnecessary 401/refresh churn.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/features/word-list/index.tsx` at line 46, The call to
useWordLists() always fetches because it defaults enabled=true; change the call
to gate the query by auth/hydration state (like use-save-to-list.ts does) so it
doesn't hit /word-lists when logged out — e.g. obtain the isAuthenticated
(and/or hasHydrated) flag from your auth hook and call
useWordLists(isAuthenticated && hasHydrated) (or pass an enabled boolean) so the
query is disabled until the user is authenticated and the client has hydrated.
libs/ui/src/components/ui/icons.tsx (1)

2-22: ⚡ Quick win

Use cn so callers can reliably override the default size.

Concatenating the default size with className (e.g. w-12 h-12 ${className}) leaves both classes in the output. With conflicting utilities like w-6/w-12, Tailwind resolves by CSS source order, not string order, so a caller-supplied size may not win. cn (tailwind-merge) dedupes conflicts correctly. This applies to all six icons.

♻️ Proposed change (apply the same pattern to each icon)
+import { cn } from "`@ielts/ui/lib/utils`"
+
 export const TrophyIcon = ({ className, ...props }: React.SVGProps<SVGSVGElement>) => (
   <svg
     ...
-    className={`w-12 h-12 ${className || ''}`}
+    className={cn("w-12 h-12", className)}
     {...props}
   >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/ui/src/components/ui/icons.tsx` around lines 2 - 22, The TrophyIcon (and
the other five icon components) currently concatenate default size classes with
caller className which prevents caller sizes from reliably winning; import and
use the project's class-name merge helper (cn) or tailwind-merge and replace
className={`w-12 h-12 ${className || ''}`} with className={cn('w-12 h-12',
className)} in TrophyIcon (and apply the same pattern to the other icon
components) so conflicting utilities like w-6/w-12 are deduped correctly; also
add the necessary import for cn at the top of the file.
libs/ui/src/components/ui/toast.tsx (1)

83-83: ⚡ Quick win

Use data-toast-close instead of non-standard attribute.

The toast-close="" attribute doesn't follow HTML5 conventions. If this is meant as a selector or marker, it should be data-toast-close="" to comply with the data-attribute standard.

✨ Suggested fix
-    toast-close=""
+    data-toast-close=""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/ui/src/components/ui/toast.tsx` at line 83, Replace the non-standard
attribute toast-close with the HTML5 data attribute data-toast-close in the
Toast component JSX (the occurrence in libs/ui/src/components/ui/toast.tsx), and
update any corresponding selectors or references that rely on "toast-close"
(e.g., tests, querySelector calls, or CSS targeting) to use "data-toast-close"
instead so the marker/selector remains consistent and standards-compliant.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/backend/src/config/mongo.ts`:
- Around line 13-17: The code calls dns.setServers(['8.8.8.8','1.1.1.1']) which
mutates process-global DNS and can break other services; instead guard that call
behind a runtime toggle (e.g., an env var like ENABLE_CUSTOM_DNS) or replace
with a scoped resolver used only for Mongo lookups, and remove the unconditional
global mutation and its try/catch as the guard for later SRV resolution
failures; locate the dns.setServers call in the mongo config (symbol:
dns.setServers) and wrap it so the global DNS change only happens when the env
flag is explicitly set (or implement a custom resolver for SRV lookups used by
your Mongo connect logic).

In `@apps/user/src/app/`(main)/not-found.tsx:
- Around line 57-65: The Link currently wraps the Button creating nested
interactive elements (an anchor containing a button), so change this to render
the Button as the parent and pass the Link as its child using the Button's
asChild prop: replace the outer <Link href="/vocabulary" ...> wrapper with
<Button asChild ...> and move <Link href="/vocabulary"> around the ArrowLeft +
text inside the Button, preserving Button props (size="lg", className, etc.) and
the ArrowLeft icon; ensure Link has no button-specific classes so only one
interactive element is rendered (Button) and the accessible behavior remains
intact.

In `@apps/user/src/features/auth/hooks/use-login.ts`:
- Around line 44-48: The redirect guard in use-login.ts is insufficient because
it only blocks "//" but allows "/\..." (and encoded "%5c" which is decoded by
searchParams.get()); update the safeRedirect logic to only allow paths that
start with a single leading slash and whose second character (if present) is not
"/" or "\" (backslash) — reference the variables redirectTo and safeRedirect and
the router.push(safeRedirect) call; implement a check that
redirectTo.startsWith('/') AND (redirectTo.length === 1 OR (redirectTo.length >
1 AND redirectTo[1] !== '/' AND redirectTo[1] !== '\\')) and otherwise fall back
to "/" so protocol-relative or backslash-bypassed redirects are rejected.

In `@libs/ui/src/hooks/use-toast.ts`:
- Line 12: The TOAST_REMOVE_DELAY constant in use-toast is set to 1000000 (≈16.7
minutes) causing toasts to persist far too long; change TOAST_REMOVE_DELAY to a
sensible auto-dismiss interval (e.g., 3000–5000 ms) and adjust any related logic
that relies on the delay (look for TOAST_REMOVE_DELAY usage in the useToast hook
and any timer/cleanup in the same file) so toasts are removed after a few
seconds and memory/DOM cleanup happens promptly.
- Around line 177-185: The useEffect subscribing setState to listeners is
re-running because it depends on state; change the dependency array for the
effect in use-toast.ts to an empty array so the subscription persists for the
component lifetime: in the React.useEffect that pushes setState into listeners
and returns the cleanup that splices it out, remove state from the dependency
array (use []), keeping the same push/cleanup logic around listeners and
setState.

---

Minor comments:
In `@apps/user/src/app/global.css`:
- Line 303: Stylelint is failing due to camelCase `@keyframes` names; rename each
keyframe to kebab-case (e.g., marqueeScroll -> marquee-scroll, marqueeScrollRev
-> marquee-scroll-rev, testScrollUp -> test-scroll-up, testScrollDown ->
test-scroll-down, spinCW -> spin-cw, spinCCW -> spin-ccw, cursorBlink ->
cursor-blink, thinkPulse -> think-pulse, heroTextFadeIn -> hero-text-fade-in,
footerHeartBeat -> footer-heart-beat) and update every corresponding
animation/animation-name reference and related class usages (for example update
.animate-marquee, .animate-test-scroll-up, .ielts-master-btn, .ln-footer-heart
and any other .animate-* or animation declarations) so names remain in sync
across the CSS.
- Line 623: The font-family declarations use quoted single-token font name
"Geist" which triggers stylelint's font-family-name-quotes rule; update each
occurrence of font-family: "Geist", sans-serif; to remove the quotes so it reads
font-family: Geist, sans-serif; while leaving multi-word names like "Geist Mono"
quoted; search for the exact token font-family: "Geist" in the stylesheet and
replace those instances accordingly.

In `@apps/user/src/components/footer/footer.constants.ts`:
- Around line 7-57: PLATFORM_LINKS, EXAM_GUIDES, FREE_RESOURCES and STUDY_TIPS
in footer.constants.ts contain internal hrefs (e.g. /about, /contact, /feedback,
/terms, /privacy and /guides/*) that currently 404; fix by either (A) adding
matching app-router pages for each route (create page components for /about,
/contact, /feedback, /terms, /privacy and the /guides/* segments) or (B) update
the hrefs in PLATFORM_LINKS and EXAM_GUIDES to point to existing routes in the
user app; edit the array entries in footer.constants.ts (PLATFORM_LINKS,
EXAM_GUIDES) to reflect the chosen approach so all internal links resolve.

In `@apps/user/src/components/navbar/mobile-nav.tsx`:
- Around line 130-140: The dialog rendered in the motion.div (role="dialog",
aria-modal="true") needs focus management: when opening the component that uses
DROPDOWN_VARIANTS and transitionConfig, programmatically move keyboard focus
into the panel (e.g., to the first interactive element or a dedicated container)
and implement a focus trap so Tab/Shift+Tab cannot move focus to background
content; alternatively set inert on the rest of the page while open. Wire this
behavior to the component's open/close lifecycle (mount/open -> set initial
focus and enable trap or inert; unmount/close -> restore focus to the trigger)
and ensure Escape handling already present still closes and restores focus.

In `@apps/user/src/components/ReviewCard.tsx`:
- Around line 57-77: The Link around the Button in ReviewCard.tsx remains
keyboard-focusable when !isDue so Enter still navigates; update the component to
only render the Link when isDue is true (wrap the Button with <Link> when isDue)
and otherwise render a non-interactive element (e.g., a span or disabled button)
with aria-disabled="true" and tabIndex={-1} so both mouse and keyboard are
blocked; ensure this change targets the Link and Button usage and preserves the
existing className and disabled prop on Button.

In `@apps/user/src/features/auth/components/forgot-password-form.tsx`:
- Around line 42-50: The email <Input> field (id="email") is missing an
autocomplete hint; add the autoComplete="email" prop to the Input component used
in the forgot-password form (the element with id="email" and value={email}
onChange={...}) so browsers and password managers can reliably prefill the
recovery email field.

In `@apps/user/src/features/auth/components/login-form.tsx`:
- Around line 37-60: Add browser autofill hints to the login inputs by setting
autoComplete="email" on the Input with id="email" and
autoComplete="current-password" on the Input with id="password" (the components
that use setEmail and setPassword). Update those two Input props in the
LoginForm component so password managers and browsers can prefill and save
credentials.

In `@apps/user/src/features/auth/components/register-form.tsx`:
- Around line 38-76: Add HTML autocomplete hints to the registration inputs so
browsers/password managers can assist: update the Input with id "name" (where
value={name} and onChange={setName}) to include autoComplete="name", the Input
with id "email" (value={email}) to include autoComplete="email", and the Input
with id "password" (value={password}) to include autoComplete="new-password";
modify these three Input components in register-form.tsx (the
name/email/password Input elements) accordingly.

In `@apps/user/src/features/landing/components/hero-section.tsx`:
- Around line 81-89: The component starts speech with triggerAudioWave calling
speechSynthesis.speak(...) but never cancels it on unmount; update HeroSection
to cancel any in-flight speech in a cleanup effect by calling
speechSynthesis.cancel() (or cancel specific utterance if you store it) inside a
useEffect cleanup so ongoing audio is stopped when the component unmounts;
locate triggerAudioWave and add either a stored SpeechSynthesisUtterance
reference or a simple speechSynthesis.cancel() in the component's cleanup to
ensure speech stops on unmount.

In `@apps/user/src/features/landing/components/testimonials.tsx`:
- Around line 192-198: The duplicated testimonial sets created by SET_PREFIXES
are announced multiple times by screen readers; when mapping SET_PREFIXES, add
aria-hidden to the outer list item (the element with class "ln-test-col-set")
for every set except the first (use the map index to detect i > 0) so duplicates
are visual-only; keep the existing keys (e.g. `${prefix}-${testimonial.handle}`)
and leave TestimonialCard unchanged.

In `@apps/user/src/features/review/components/flashcard.tsx`:
- Around line 30-40: The Card element (the component with role="button",
tabIndex={0}) only has onClick; add an onKeyDown handler on that same Card which
checks for Enter (e.g., key === "Enter") and Space (e.g., key === " " or key ===
"Spacebar") and triggers onFlip() when !isFlipped, using event.preventDefault()
for Space to avoid page scroll; ensure the handler mirrors the onClick guard
(!isFlipped && onFlip()) so keyboard focus behaves identically to mouse clicks
and does nothing when isFlipped is true.

In `@apps/user/src/features/vocabulary/components/vocabulary-filters.tsx`:
- Around line 106-114: The clear buttons that render only the <X> icon lack
accessible names; update the button for wordSearch (the button that calls
onWordSearchChange('') and displays <X>) and the topic search clear button (the
button that calls onTopicSearchChange('') / references topicSearch) to include a
descriptive aria-label (e.g., "Clear word search" and "Clear topic search"), and
mark the Lucide <X> icon itself as decorative with aria-hidden="true" so
assistive tech reads the button label instead of the icon.

In `@apps/user/src/features/word-list/components/delete-confirmation-modal.tsx`:
- Around line 40-92: The modal lacks ARIA dialog semantics, focus trapping,
Escape-to-close, and an accessible label; update delete-confirmation-modal.tsx
to use the existing Radix-based Dialog from `@ielts/ui` (instead of the
hand-rolled createPortal/AnimatePresence/motion block) so you get
role/aria-modal, focus management and Escape handling out of the box, wiring the
Dialog.Trigger/Content/Title to the existing onClose/onConfirm handlers and
using listName for the label; if you must keep the custom implementation, add
role="dialog" aria-modal="true" give the <h3> an id and set aria-labelledby to
that id on the dialog container, attach a keydown listener to the top-level
container to call onClose on Escape, and ensure focus is trapped/returned (set
initial focus to the Cancel or Delete Button and restore focus to the opener on
close) while preserving isOpen/isPending/onClose/onConfirm behavior.

In `@apps/user/src/features/word-list/components/list-card.tsx`:
- Around line 212-223: The Popover is controlled by the open/onOpenChange pair
but the PopoverTrigger's Button also has an explicit onClick={() =>
setIsMenuOpen(true)}, which forces isMenuOpen to true and prevents Radix from
toggling/closing; remove the onClick prop from the Button inside PopoverTrigger
so that Popover, PopoverTrigger, isMenuOpen and setIsMenuOpen manage open state
consistently.

In `@apps/user/src/features/word-list/hooks/use-save-to-list.ts`:
- Around line 58-60: handleCreateAndAdd can be re-entered by rapid Enter
presses; add an early return that checks the isCreating flag at the top of
handleCreateAndAdd (e.g., if (isCreating) return) and set isCreating = true
immediately before any await (and reset it in a finally block) so createList
cannot be invoked concurrently; reference the handleCreateAndAdd function, the
isCreating state, and the createList call to locate where to add the guard and
the early state toggle.

In `@libs/ui/src/lib/WordDetailsModal.tsx`:
- Line 43: The DialogContent currently sets aria-describedby={undefined}, which
removes an accessibility description; update the modal to either remove the
aria-describedby prop so Radix can handle it, or add a proper DialogDescription
element tied to DialogContent (e.g., give DialogDescription an id like
"word-description" and reference that id from DialogContent) to describe the
dialog content (include the specific word via word.word and mark the description
visually hidden with a sr-only class if needed); adjust the DialogContent/
DialogDescription usage so screen readers receive the descriptive text instead
of undefined.

---

Nitpick comments:
In `@apps/user/src/features/word-list/index.tsx`:
- Line 46: The call to useWordLists() always fetches because it defaults
enabled=true; change the call to gate the query by auth/hydration state (like
use-save-to-list.ts does) so it doesn't hit /word-lists when logged out — e.g.
obtain the isAuthenticated (and/or hasHydrated) flag from your auth hook and
call useWordLists(isAuthenticated && hasHydrated) (or pass an enabled boolean)
so the query is disabled until the user is authenticated and the client has
hydrated.

In `@libs/ui/src/components/ui/icons.tsx`:
- Around line 2-22: The TrophyIcon (and the other five icon components)
currently concatenate default size classes with caller className which prevents
caller sizes from reliably winning; import and use the project's class-name
merge helper (cn) or tailwind-merge and replace className={`w-12 h-12
${className || ''}`} with className={cn('w-12 h-12', className)} in TrophyIcon
(and apply the same pattern to the other icon components) so conflicting
utilities like w-6/w-12 are deduped correctly; also add the necessary import for
cn at the top of the file.

In `@libs/ui/src/components/ui/toast.tsx`:
- Line 83: Replace the non-standard attribute toast-close with the HTML5 data
attribute data-toast-close in the Toast component JSX (the occurrence in
libs/ui/src/components/ui/toast.tsx), and update any corresponding selectors or
references that rely on "toast-close" (e.g., tests, querySelector calls, or CSS
targeting) to use "data-toast-close" instead so the marker/selector remains
consistent and standards-compliant.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a3e18d31-006c-4e50-a4fb-c9667bcaa7b3

📥 Commits

Reviewing files that changed from the base of the PR and between ffac6ae and 932ebb0.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (145)
  • apps/backend/src/config/mongo.ts
  • apps/backend/src/modules/quiz/quiz-analytics.service.ts
  • apps/user/components.json
  • apps/user/next.config.js
  • apps/user/package.json
  • apps/user/src/app/(auth)/forgot-password/forgot-password-content.tsx
  • apps/user/src/app/(auth)/forgot-password/page.tsx
  • apps/user/src/app/(auth)/layout.tsx
  • apps/user/src/app/(auth)/login/login-content.tsx
  • apps/user/src/app/(auth)/login/page.tsx
  • apps/user/src/app/(auth)/register/page.tsx
  • apps/user/src/app/(auth)/register/register-content.tsx
  • apps/user/src/app/(auth)/reset-password/page.tsx
  • apps/user/src/app/(auth)/reset-password/reset-password-content.tsx
  • apps/user/src/app/(main)/analytics/page.tsx
  • apps/user/src/app/(main)/dashboard/page.tsx
  • apps/user/src/app/(main)/layout.tsx
  • apps/user/src/app/(main)/my-lists/page.tsx
  • apps/user/src/app/(main)/not-found.tsx
  • apps/user/src/app/(main)/page.tsx
  • apps/user/src/app/(main)/profile/page.tsx
  • apps/user/src/app/(main)/quiz/page.tsx
  • apps/user/src/app/(main)/review/page.tsx
  • apps/user/src/app/(main)/vocabulary/@modal/(.)[id]/page.tsx
  • apps/user/src/app/(main)/vocabulary/@modal/default.tsx
  • apps/user/src/app/(main)/vocabulary/[id]/loading.tsx
  • apps/user/src/app/(main)/vocabulary/[id]/page.tsx
  • apps/user/src/app/(main)/vocabulary/default.tsx
  • apps/user/src/app/(main)/vocabulary/layout.tsx
  • apps/user/src/app/(main)/vocabulary/page.tsx
  • apps/user/src/app/forgot-password/forgot-password-content.tsx
  • apps/user/src/app/global.css
  • apps/user/src/app/layout.tsx
  • apps/user/src/app/login/login-content.tsx
  • apps/user/src/app/not-found.tsx
  • apps/user/src/app/register/register-content.tsx
  • apps/user/src/app/reset-password/reset-password-content.tsx
  • apps/user/src/app/sitemap.ts
  • apps/user/src/app/vocabulary/[id]/loading.tsx
  • apps/user/src/components/MainContent.tsx
  • apps/user/src/components/ReviewCard.tsx
  • apps/user/src/components/footer/UserFooter.tsx
  • apps/user/src/components/footer/footer.constants.ts
  • apps/user/src/components/navbar/desktop-nav.tsx
  • apps/user/src/components/navbar/mobile-nav.tsx
  • apps/user/src/components/navbar/nav-links.tsx
  • apps/user/src/components/navbar/navbar.tsx
  • apps/user/src/components/navbar/use-navbar.ts
  • apps/user/src/features/analytics/components/analytics-empty-state.tsx
  • apps/user/src/features/analytics/components/analytics-skeleton.tsx
  • apps/user/src/features/analytics/components/analytics-stat-card.tsx
  • apps/user/src/features/analytics/components/difficulty-chart.tsx
  • apps/user/src/features/analytics/components/performance-chart.tsx
  • apps/user/src/features/analytics/components/recent-attempts-table.tsx
  • apps/user/src/features/analytics/index.tsx
  • apps/user/src/features/analytics/services/analytics.api.ts
  • apps/user/src/features/auth/components/forgot-password-form.tsx
  • apps/user/src/features/auth/components/forgot-password-success.tsx
  • apps/user/src/features/auth/components/login-form.tsx
  • apps/user/src/features/auth/components/register-form.tsx
  • apps/user/src/features/auth/components/reset-password-form.tsx
  • apps/user/src/features/auth/hooks/use-login.ts
  • apps/user/src/features/auth/hooks/use-password-recovery.ts
  • apps/user/src/features/auth/hooks/use-register.ts
  • apps/user/src/features/dashboard/components/action-card.tsx
  • apps/user/src/features/dashboard/components/dashboard-skeleton.tsx
  • apps/user/src/features/dashboard/components/dashboard-stats.tsx
  • apps/user/src/features/dashboard/components/quick-actions.tsx
  • apps/user/src/features/dashboard/components/stat-card.tsx
  • apps/user/src/features/dashboard/index.tsx
  • apps/user/src/features/landing/components/cta.tsx
  • apps/user/src/features/landing/components/dot-field.tsx
  • apps/user/src/features/landing/components/features-bento.tsx
  • apps/user/src/features/landing/components/features-section.tsx
  • apps/user/src/features/landing/components/hero-band.tsx
  • apps/user/src/features/landing/components/hero-section.tsx
  • apps/user/src/features/landing/components/how-it-works.tsx
  • apps/user/src/features/landing/components/stats-section.tsx
  • apps/user/src/features/landing/components/testimonials.tsx
  • apps/user/src/features/landing/index.tsx
  • apps/user/src/features/profile/components/change-password-form.tsx
  • apps/user/src/features/profile/components/profile-error-state.tsx
  • apps/user/src/features/profile/components/profile-info-card.tsx
  • apps/user/src/features/profile/components/profile-skeleton.tsx
  • apps/user/src/features/profile/components/update-profile-form.tsx
  • apps/user/src/features/profile/hooks/use-profile.ts
  • apps/user/src/features/profile/index.tsx
  • apps/user/src/features/quiz/components/quiz-question-card.tsx
  • apps/user/src/features/quiz/components/quiz-results.tsx
  • apps/user/src/features/quiz/components/quiz-start-screen.tsx
  • apps/user/src/features/quiz/hooks/use-quiz-game.ts
  • apps/user/src/features/quiz/index.tsx
  • apps/user/src/features/review/components/flashcard.tsx
  • apps/user/src/features/review/components/rating-buttons.tsx
  • apps/user/src/features/review/components/review-complete.tsx
  • apps/user/src/features/review/components/review-header.tsx
  • apps/user/src/features/review/components/review-loading.tsx
  • apps/user/src/features/review/hooks/use-review-session.ts
  • apps/user/src/features/review/index.tsx
  • apps/user/src/features/vocabulary/components/vocabulary-filters.tsx
  • apps/user/src/features/vocabulary/components/vocabulary-list.tsx
  • apps/user/src/features/vocabulary/components/vocabulary-page-skeleton.tsx
  • apps/user/src/features/vocabulary/components/word-details-view.tsx
  • apps/user/src/features/vocabulary/index.tsx
  • apps/user/src/features/vocabulary/services/server-vocabulary.api.ts
  • apps/user/src/features/word-list/components/delete-confirmation-modal.tsx
  • apps/user/src/features/word-list/components/list-card.tsx
  • apps/user/src/features/word-list/components/lists-stats.tsx
  • apps/user/src/features/word-list/components/save-to-list-button.tsx
  • apps/user/src/features/word-list/hooks/use-save-to-list.ts
  • apps/user/src/features/word-list/index.tsx
  • apps/user/src/features/word-list/services/word-list.api.ts
  • apps/user/src/lib/get-query-client.ts
  • apps/user/src/proxy.ts
  • apps/user/tailwind.config.js
  • apps/user/tsconfig.tsbuildinfo
  • libs/ui/src/components/auth/ResetPasswordForm.tsx
  • libs/ui/src/components/pronunciation-button.tsx
  • libs/ui/src/components/ui/avatar.tsx
  • libs/ui/src/components/ui/badge.tsx
  • libs/ui/src/components/ui/button.tsx
  • libs/ui/src/components/ui/card.tsx
  • libs/ui/src/components/ui/dialog.tsx
  • libs/ui/src/components/ui/dropdown-menu.tsx
  • libs/ui/src/components/ui/icons.tsx
  • libs/ui/src/components/ui/input.tsx
  • libs/ui/src/components/ui/label.tsx
  • libs/ui/src/components/ui/popover.tsx
  • libs/ui/src/components/ui/progress.tsx
  • libs/ui/src/components/ui/select.tsx
  • libs/ui/src/components/ui/separator.tsx
  • libs/ui/src/components/ui/skeleton.tsx
  • libs/ui/src/components/ui/table.tsx
  • libs/ui/src/components/ui/toast.tsx
  • libs/ui/src/components/ui/toaster.tsx
  • libs/ui/src/hooks/use-toast.ts
  • libs/ui/src/index.ts
  • libs/ui/src/lib/Pagination.tsx
  • libs/ui/src/lib/UserMenu.tsx
  • libs/ui/src/lib/WordDetailsModal.tsx
  • libs/ui/src/lib/page-loading-fallback.tsx
  • libs/ui/src/lib/utils.ts
  • libs/ui/src/theme/index.ts
  • package.json
  • tsconfig.base.json
💤 Files with no reviewable changes (7)
  • apps/user/src/app/forgot-password/forgot-password-content.tsx
  • apps/user/src/app/vocabulary/[id]/loading.tsx
  • apps/user/src/app/register/register-content.tsx
  • apps/user/src/app/not-found.tsx
  • apps/user/src/features/landing/components/features-section.tsx
  • apps/user/src/app/reset-password/reset-password-content.tsx
  • apps/user/src/app/login/login-content.tsx

Comment thread apps/backend/src/config/mongo.ts Outdated
Comment thread apps/user/src/app/(main)/not-found.tsx Outdated
Comment thread apps/user/src/features/auth/hooks/use-login.ts
Comment thread libs/ui/src/hooks/use-toast.ts Outdated
Comment thread libs/ui/src/hooks/use-toast.ts Outdated
…and animation

- Remove padding from ln-test-section and adjust padding in ln-cta-section for consistent spacing
- Align footer columns to flex-start and change footer link to inline-block with adjusted icon styles
- Remove animated heartbeat from footer heart icon and update hover color for footer creator link
- Refactor footer navigation layout to use CSS grid for better responsiveness and spacing
- Add new 404 Not Found page styles with noise overlay, animated card border, and responsive design
- Implement NotFound React component with semantic structure, definition, example, synonyms, and action buttons
- Remove subtle premium glows from auth layout for cleaner auth pages
- Increase horizontal padding in profile, review, and word-list pages for improved layout
- Fix toast viewport position from top to bottom and correct toast animation variants for slide-in direction

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/user/src/app/global.css`:
- Line 1445: Remove the unnecessary quotes around the single-token font family
"Geist" in the font-family declarations; locate the occurrences of the
declaration using the exact token font-family: 'Geist', sans-serif; (appearing
multiple times) and change them to use the unquoted identifier (font-family:
Geist, sans-serif;) so they comply with the stylelint rule
font-family-name-quotes for single-word family names.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3821fff6-7668-4438-a630-fb93b2870d38

📥 Commits

Reviewing files that changed from the base of the PR and between 932ebb0 and 7322de9.

⛔ Files ignored due to path filters (2)
  • apps/user/src/app/apple-icon.png is excluded by !**/*.png
  • apps/user/src/app/icon.png is excluded by !**/*.png
📒 Files selected for processing (7)
  • apps/user/src/app/(auth)/layout.tsx
  • apps/user/src/app/global.css
  • apps/user/src/app/not-found.tsx
  • apps/user/src/features/profile/index.tsx
  • apps/user/src/features/review/index.tsx
  • apps/user/src/features/word-list/index.tsx
  • libs/ui/src/components/ui/toast.tsx
💤 Files with no reviewable changes (1)
  • apps/user/src/app/(auth)/layout.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/user/src/features/review/index.tsx
  • libs/ui/src/components/ui/toast.tsx
  • apps/user/src/features/word-list/index.tsx
  • apps/user/src/features/profile/index.tsx

Comment thread apps/user/src/app/global.css Outdated
- Increase padding for .ln-test-section and .ln-cta-section in global.css with responsive adjustments
- Remove unused authentication check in HeroSection and unify call-to-action button text to "Start Learning"
- Reduce vertical padding in FeaturesBentoSection for better spacing consistency
- Add definition, example, synonyms, and antonyms display for hero word cards with animated transitions
- Update hero word data structure to include new linguistic details replacing phonetic notation
- Improve hero word layout with added gaps and styling refinements
- Add vertical padding to HowItWorks and Stats sections for improved visual separation
- Minor classname and structural tweaks in landing container and hero section components

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/user/src/features/landing/components/hero-section.tsx (2)

91-99: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use PointerEvent type for pointerdown event.

The event listener uses pointerdown but types the callback parameter as MouseEvent. While this works (since PointerEvent extends MouseEvent), it's technically incorrect.

🔧 Proposed fix
-    const onClickOutside = (e: MouseEvent) => {
+    const onClickOutside = (e: PointerEvent) => {
       if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/features/landing/components/hero-section.tsx` around lines 91 -
99, In React.useEffect where onClickOutside is defined, the event parameter is
incorrectly typed as MouseEvent for a 'pointerdown' listener; change the handler
signature to use PointerEvent (e.g., const onClickOutside = (e: PointerEvent) =>
{ ... }) so the type matches the event being listened to, keep the existing
contains(e.target as Node) check and the addEventListener/removeEventListener
calls referencing 'pointerdown', and update any references to onClickOutside,
dropdownRef, and setIsDropdownOpen accordingly.

63-84: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Add cleanup to cancel ongoing speech.

The SpeechSynthesis utterance is not canceled when the component unmounts or when currentWord changes. This can cause:

  • setState calls on an unmounted component (React warning)
  • Speech for the wrong word continuing after a category switch
  • Potential memory leaks
🔧 Proposed fix: Add cleanup effect

Add a cleanup effect to cancel speech on unmount or word change:

+  React.useEffect(() => {
+    return () => {
+      if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
+        window.speechSynthesis.cancel();
+      }
+    };
+  }, [currentWord.word]);
+
   const triggerAudioWave = () => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/features/landing/components/hero-section.tsx` around lines 63 -
84, The SpeechSynthesis started by triggerAudioWave isn't being cancelled on
unmount or when currentWord changes, causing lingering speech and
setState-after-unmount; fix by adding a useEffect that depends on currentWord
(and runs on mount) which cancels any ongoing speech via
window.speechSynthesis.cancel(), clears/reset isAudioPlaying (call
setIsAudioPlaying(false)), and removes/cleans any handlers tied to the
SpeechSynthesisUtterance (store the utterance in a ref used by triggerAudioWave
so the effect can remove onend/onerror and cancel the utterance) to ensure
speech stops on unmount or when currentWord updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/user/src/features/landing/components/hero-section.tsx`:
- Around line 91-99: In React.useEffect where onClickOutside is defined, the
event parameter is incorrectly typed as MouseEvent for a 'pointerdown' listener;
change the handler signature to use PointerEvent (e.g., const onClickOutside =
(e: PointerEvent) => { ... }) so the type matches the event being listened to,
keep the existing contains(e.target as Node) check and the
addEventListener/removeEventListener calls referencing 'pointerdown', and update
any references to onClickOutside, dropdownRef, and setIsDropdownOpen
accordingly.
- Around line 63-84: The SpeechSynthesis started by triggerAudioWave isn't being
cancelled on unmount or when currentWord changes, causing lingering speech and
setState-after-unmount; fix by adding a useEffect that depends on currentWord
(and runs on mount) which cancels any ongoing speech via
window.speechSynthesis.cancel(), clears/reset isAudioPlaying (call
setIsAudioPlaying(false)), and removes/cleans any handlers tied to the
SpeechSynthesisUtterance (store the utterance in a ref used by triggerAudioWave
so the effect can remove onend/onerror and cancel the utterance) to ensure
speech stops on unmount or when currentWord updates.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 54f7f171-d7da-41a8-9907-0188082a6f7e

📥 Commits

Reviewing files that changed from the base of the PR and between 7322de9 and e9ccf56.

📒 Files selected for processing (6)
  • apps/user/src/app/global.css
  • apps/user/src/features/landing/components/features-bento.tsx
  • apps/user/src/features/landing/components/hero-section.tsx
  • apps/user/src/features/landing/components/how-it-works.tsx
  • apps/user/src/features/landing/components/stats-section.tsx
  • apps/user/src/features/landing/index.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/user/src/features/landing/components/stats-section.tsx
  • apps/user/src/features/landing/components/how-it-works.tsx
  • apps/user/src/features/landing/index.tsx
  • apps/user/src/app/global.css

…d components

- Add About page with metadata and composed AboutContent including hero, features, guarantees, developer, and disclaimer sections
- Implement Contact page with metadata and ContactContent including hero, info, categories, and tips components
- Create Feedback page with metadata and FeedbackContent component
- Add Privacy Policy page with metadata and PrivacyContent component
- Add Terms & Conditions page with metadata and TermsContent component
- Develop reusable components with animations using framer-motion and icons with lucide-react
- Include detailed social links for developer profile and structured inquiry categories for contact page
- Enhance user support with pro tips and categorized contact information for better user communication

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
apps/user/src/components/contact/contact-categories.tsx (1)

11-18: ⚡ Quick win

Refactor fragile string parsing of accentColor property.

The accentColor property is used both as a full class string (line 118) and parsed via .split(' ')[2] (line 136) to extract the text color. This is brittle because it assumes a specific order and format of space-separated classes.

If someone reorders the classes in the CATEGORIES constant (lines 31, 45, 58), the icon colors will break silently.

♻️ Recommended refactor: split into separate properties

Update the CategoryItem interface and CATEGORIES constant to use separate, explicit properties:

 interface CategoryItem {
   readonly icon: ComponentType<{ readonly size?: number; readonly className?: string }>;
   readonly title: string;
   readonly description: string;
   readonly details?: readonly string[];
   readonly colSpan: string;
-  readonly accentColor: string;
+  readonly gradientClasses: string;
+  readonly textColorClass: string;
+  readonly borderColorClass: string;
 }

Then update CATEGORIES:

 const CATEGORIES: readonly CategoryItem[] = [
   {
     icon: HelpCircle,
     title: 'General Inquiries',
     description: '...',
     colSpan: 'md:col-span-12 lg:col-span-4',
-    accentColor: 'from-blue-500/20 to-indigo-500/20 text-blue-400 border-blue-500/20',
+    gradientClasses: 'from-blue-500/20 to-indigo-500/20',
+    textColorClass: 'text-blue-400',
+    borderColorClass: 'border-blue-500/20',
   },
   // ... repeat for other categories
 ];

And update usage:

-<div className={`inline-flex p-3 rounded-xl bg-gradient-to-tr ${category.accentColor} border`}>
+<div className={`inline-flex p-3 rounded-xl bg-gradient-to-tr ${category.gradientClasses} ${category.textColorClass} ${category.borderColorClass} border`}>

-className={`flex-shrink-0 mt-0.5 ${category.accentColor.split(' ')[2]}`}
+className={`flex-shrink-0 mt-0.5 ${category.textColorClass}`}

Also applies to: 136-136

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/components/contact/contact-categories.tsx` around lines 11 -
18, The accentColor string is being used both as a full class list and parsed
with .split(' ')[2], which is brittle; update the CategoryItem interface to
replace accentColor with two explicit properties (e.g., accentClass for the full
container/background classes and accentTextClass for the icon/text color class),
update the CATEGORIES constant entries to provide these two properties for each
category instead of a single space-separated string, and then change any usages
(the container that currently uses category.accentColor and the place that used
.split to derive the icon color) to use category.accentClass and
category.accentTextClass respectively (refer to CategoryItem, CATEGORIES and the
component code where icon class and container class are applied).
apps/user/src/components/terms/terms-hero.tsx (1)

46-46: 💤 Low value

Consider extracting the last-updated date to a constant.

The hard-coded date "May 30, 2026" could be moved to a constant at the file or module level for easier maintenance when the terms are updated.

♻️ Suggested refactor
+const LAST_UPDATED = 'May 30, 2026';
+
 // ============================================================================
 // Component
 // ============================================================================
 
 export function TermsHero() {
   return (
     <motion.header
       ...
     >
       ...
       <Badge
         variant="secondary"
         className="mt-6 px-4 py-2 text-xs sm:text-sm font-normal text-zinc-400 rounded-full"
       >
-        📅 Last Updated: May 30, 2026
+        📅 Last Updated: {LAST_UPDATED}
       </Badge>
     </motion.header>
   );
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/components/terms/terms-hero.tsx` at line 46, Extract the
hard-coded string "May 30, 2026" into a module-level constant (e.g., const
TERMS_LAST_UPDATED = "May 30, 2026") and replace the inline text in the
TermsHero component (or the JSX that renders "📅 Last Updated: ...") with that
constant; place the constant near the top of
apps/user/src/components/terms/terms-hero.tsx so future updates only require
changing the one symbol (TERMS_LAST_UPDATED) and ensure the JSX uses the
constant rather than the literal string.
apps/user/src/components/privacy/privacy-content.tsx (1)

14-14: ⚡ Quick win

Memoize the section IDs array to prevent unnecessary effect re-runs.

The .map() call creates a new array on every render, causing useScrollSpy's effect to re-register the scroll listener unnecessarily (Line 52 of use-scroll-spy.ts depends on ids).

♻️ Proposed fix

Option 1 (preferred): Extract to a module-level constant

+const SECTION_IDS = PRIVACY_SECTIONS.map((section) => section.id);
+
 export function PrivacyContent() {
-  const sectionIds = PRIVACY_SECTIONS.map((section) => section.id);
-  const activeId = useScrollSpy(sectionIds);
+  const activeId = useScrollSpy(SECTION_IDS);

Option 2: Use useMemo

+import { useMemo } from 'react';
+
 export function PrivacyContent() {
-  const sectionIds = PRIVACY_SECTIONS.map((section) => section.id);
+  const sectionIds = useMemo(() => PRIVACY_SECTIONS.map((section) => section.id), []);
   const activeId = useScrollSpy(sectionIds);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/components/privacy/privacy-content.tsx` at line 14,
PRIVACY_SECTIONS.map(...) is creating a new array every render which triggers
useScrollSpy to re-run; fix by memoizing the IDs—either extract the mapped
result to a module-level constant (derive SECTION_IDS from PRIVACY_SECTIONS) or
wrap the mapping in useMemo in privacy-content.tsx so sectionIds is stable
across renders; ensure the stable identifier (sectionIds) passed into
useScrollSpy remains referentially equal unless PRIVACY_SECTIONS changes.
apps/user/src/components/feedback/feedback-form.tsx (1)

119-120: Reminder: Backend integration pending.

The TODO indicates that backend feedback submission is not yet implemented. The current implementation uses a simulated delay for demonstration purposes.

Do you want me to help draft the backend integration code or open a tracking issue for this task?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/components/feedback/feedback-form.tsx` around lines 119 - 120,
The form currently uses a simulated delay instead of submitting to the backend;
replace the simulated wait in the submit handler (e.g., handleSubmit or
sendFeedback in feedback-form.tsx) with an actual POST to your feedback API
(e.g., axios.post('/api/feedback', { feedbackType, rating, message, email,
deviceInfo })), ensure you set and clear the loading/disabled state around the
request, handle and display errors from the response (and log them), and keep
the success flow (clearing the form/displaying confirmation) on 2xx responses;
extract deviceInfo via the existing helper or add getDeviceInfo() if missing and
validate payload fields before sending.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/user/src/components/feedback/feedback-categories.tsx`:
- Around line 11-19: The code uses a fragile space-splitting pattern on
accentClass to extract Tailwind classes; update the FeedbackCategory interface
to replace accentClass with typed properties (gradientClasses, textColorClass,
borderColorClass, iconColorClass), update every category object (the ones
currently defining accentClass) to supply those new properties, and change the
rendering code that calls .split() (the places currently referencing accentClass
and indexing into split results) to use the new properties directly (e.g., use
gradientClasses, textColorClass, borderColorClass, iconColorClass) so the UI no
longer relies on string position.

In `@apps/user/src/components/privacy/privacy-sidebar.tsx`:
- Line 24: The sidebar uses a hardcoded offset variable named offset (currently
120) that is inconsistent with the scrollspy default of 140; update the offset
in privacy-sidebar (the offset constant in the component) to 140 or, better,
import/use a shared OFFSET constant used by use-scroll-spy so both the scroll
target and the scrollspy (use-scroll-spy) use the same value to keep active
highlighting in sync.

In `@apps/user/src/components/terms/terms-sidebar.tsx`:
- Around line 20-35: The scroll target is computed incorrectly in handleClick:
remove the bodyRect subtraction and compute the element's document position
using its viewport top plus the current page scroll (e.g.,
element.getBoundingClientRect().top + window.pageYOffset or window.scrollY),
then subtract the existing offset (120) to get offsetPosition; update the
window.scrollTo call to use that offsetPosition so the smooth scroll lands at
the proper document position.

---

Nitpick comments:
In `@apps/user/src/components/contact/contact-categories.tsx`:
- Around line 11-18: The accentColor string is being used both as a full class
list and parsed with .split(' ')[2], which is brittle; update the CategoryItem
interface to replace accentColor with two explicit properties (e.g., accentClass
for the full container/background classes and accentTextClass for the icon/text
color class), update the CATEGORIES constant entries to provide these two
properties for each category instead of a single space-separated string, and
then change any usages (the container that currently uses category.accentColor
and the place that used .split to derive the icon color) to use
category.accentClass and category.accentTextClass respectively (refer to
CategoryItem, CATEGORIES and the component code where icon class and container
class are applied).

In `@apps/user/src/components/feedback/feedback-form.tsx`:
- Around line 119-120: The form currently uses a simulated delay instead of
submitting to the backend; replace the simulated wait in the submit handler
(e.g., handleSubmit or sendFeedback in feedback-form.tsx) with an actual POST to
your feedback API (e.g., axios.post('/api/feedback', { feedbackType, rating,
message, email, deviceInfo })), ensure you set and clear the loading/disabled
state around the request, handle and display errors from the response (and log
them), and keep the success flow (clearing the form/displaying confirmation) on
2xx responses; extract deviceInfo via the existing helper or add getDeviceInfo()
if missing and validate payload fields before sending.

In `@apps/user/src/components/privacy/privacy-content.tsx`:
- Line 14: PRIVACY_SECTIONS.map(...) is creating a new array every render which
triggers useScrollSpy to re-run; fix by memoizing the IDs—either extract the
mapped result to a module-level constant (derive SECTION_IDS from
PRIVACY_SECTIONS) or wrap the mapping in useMemo in privacy-content.tsx so
sectionIds is stable across renders; ensure the stable identifier (sectionIds)
passed into useScrollSpy remains referentially equal unless PRIVACY_SECTIONS
changes.

In `@apps/user/src/components/terms/terms-hero.tsx`:
- Line 46: Extract the hard-coded string "May 30, 2026" into a module-level
constant (e.g., const TERMS_LAST_UPDATED = "May 30, 2026") and replace the
inline text in the TermsHero component (or the JSX that renders "📅 Last
Updated: ...") with that constant; place the constant near the top of
apps/user/src/components/terms/terms-hero.tsx so future updates only require
changing the one symbol (TERMS_LAST_UPDATED) and ensure the JSX uses the
constant rather than the literal string.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 98dcbe88-0425-4682-9ab3-419601b290db

📥 Commits

Reviewing files that changed from the base of the PR and between e9ccf56 and 3f646ab.

⛔ Files ignored due to path filters (1)
  • apps/user/public/dev.png is excluded by !**/*.png
📒 Files selected for processing (35)
  • apps/user/src/app/(main)/about/page.tsx
  • apps/user/src/app/(main)/contact/page.tsx
  • apps/user/src/app/(main)/feedback/page.tsx
  • apps/user/src/app/(main)/privacy/page.tsx
  • apps/user/src/app/(main)/terms/page.tsx
  • apps/user/src/components/about/about-bento.tsx
  • apps/user/src/components/about/about-content.tsx
  • apps/user/src/components/about/about-developer.tsx
  • apps/user/src/components/about/about-disclaimer.tsx
  • apps/user/src/components/about/about-guarantees.tsx
  • apps/user/src/components/about/about-hero.tsx
  • apps/user/src/components/contact/contact-categories.tsx
  • apps/user/src/components/contact/contact-content.tsx
  • apps/user/src/components/contact/contact-hero.tsx
  • apps/user/src/components/contact/contact-info.tsx
  • apps/user/src/components/contact/contact-tips.tsx
  • apps/user/src/components/feedback/feedback-categories.tsx
  • apps/user/src/components/feedback/feedback-content.tsx
  • apps/user/src/components/feedback/feedback-form.tsx
  • apps/user/src/components/feedback/feedback-hero.tsx
  • apps/user/src/components/feedback/feedback-usage.tsx
  • apps/user/src/components/privacy/privacy-content.tsx
  • apps/user/src/components/privacy/privacy-hero.tsx
  • apps/user/src/components/privacy/privacy-sections.tsx
  • apps/user/src/components/privacy/privacy-sidebar.tsx
  • apps/user/src/components/privacy/privacy.constants.tsx
  • apps/user/src/components/terms/terms-content.tsx
  • apps/user/src/components/terms/terms-hero.tsx
  • apps/user/src/components/terms/terms-sections.tsx
  • apps/user/src/components/terms/terms-sidebar.tsx
  • apps/user/src/components/terms/terms.constants.tsx
  • apps/user/src/components/terms/use-scroll-spy.ts
  • libs/auth/src/lib/api.ts
  • libs/ui/src/components/ui/textarea.tsx
  • libs/ui/src/index.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/user/src/app/(main)/contact/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • libs/ui/src/index.ts

Comment thread apps/user/src/components/feedback/feedback-categories.tsx
Comment thread apps/user/src/components/privacy/privacy-sidebar.tsx Outdated
Comment thread apps/user/src/components/terms/terms-sidebar.tsx Outdated
- Create new guide pages for Academic vs General Training, Band Scores, Listening & Reading Scoring,
  Speaking Criteria, Test Format, and Writing Criteria with metadata and content components
- Implement scroll spy hooks for active section tracking in all guide contents
- Develop reusable hero, sections, and sidebar components with smooth scroll and animation effects
- Add detailed guide constants including structured sections, tables, and explanatory content
- Use framer-motion for entrance animations of headings and content sections
- Style components for responsive layouts with consistent container max widths and spacing
- Include official references and disclaimers for content validity and trademark notices

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (4)
apps/user/src/components/guides/listening-reading-scoring/listening-reading-scoring-sidebar.tsx (1)

19-70: 🏗️ Heavy lift

Extract a shared GuideSidebar to remove repeated logic.

This component is effectively duplicated across multiple guide sidebars (same click logic, layout, and styles), which increases maintenance risk when behavior changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/user/src/components/guides/listening-reading-scoring/listening-reading-scoring-sidebar.tsx`
around lines 19 - 70, The ListeningReadingScoringSidebar duplicates common
sidebar logic (click-to-scroll, layout and styles); extract a reusable
GuideSidebar component and replace ListeningReadingScoringSidebar to use it:
move the click handler logic (handleClick) and scroll offset calculation into
the new GuideSidebar, accept props for sections (array of {id, number, title}),
activeId, and an optional renderItem or children slot for item content, and keep
the same classNames/structure so existing behavior and styling (smooth scroll,
offset, active class handling) remain unchanged; update
ListeningReadingScoringSidebar to delegate rendering of the nav/ul to
GuideSidebar and pass through sections and activeId.
apps/user/src/components/guides/speaking-criteria/speaking-criteria-sections.tsx (1)

18-71: 🏗️ Heavy lift

Consider consolidating the duplicated guide section/hero scaffolding.

This component (plus SECTION_VARIANTS/TRANSITION_CONFIG) is essentially identical to listening-reading-scoring-sections.tsx and the other guide *-sections.tsx/*-hero.tsx files; only the imported GuideSectionData type and copy differ. Since each guide already exposes a uniform GUIDE_SECTIONS: readonly GuideSectionData[], a single generic GuideSections/GuideHero (and a shared animation-constants module) would remove the repetition and keep animation timing consistent in one place. Reasonable to defer, but worth doing while the surface is fresh.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/user/src/components/guides/speaking-criteria/speaking-criteria-sections.tsx`
around lines 18 - 71, This file duplicates section/hero scaffolding and
animation constants (SECTION_VARIANTS, TRANSITION_CONFIG) that other components
(e.g., listening-reading-scoring-sections and other *-sections/*-hero files)
share; refactor by extracting the animation constants into a shared module and
creating a generic component (e.g., GuideSections and GuideHero) that accepts
readonly GuideSectionData[] (GUIDE_SECTIONS) and renders the same structure,
then replace SpeakingCriteriaSections to import and use the generic
GuideSections with its specific GuideSectionData type and content; ensure unique
IDs/aria labels (heading-${section.id}) and props (sections) remain supported.
apps/user/src/components/guides/band-scores/band-scores.constants.tsx (1)

62-69: 💤 Low value

Hardcoded last-row index is brittle when the data arrays change.

Each table toggles border-none on a literal terminal index (idx === 9 here, === 2, === 3, === 4 in the other tables). If a row is added/removed, the bottom-border styling silently breaks. Deriving from the array length avoids that coupling.

♻️ Use the array length
-              ].map((row, idx) => (
-                <TableRow key={row.band} className={`hover:bg-zinc-800/20 ${idx === 9 ? 'border-none' : 'border-[var(--rb-border-subtle)]'}`}>
+              ].map((row, idx, arr) => (
+                <TableRow key={row.band} className={`hover:bg-zinc-800/20 ${idx === arr.length - 1 ? 'border-none' : 'border-[var(--rb-border-subtle)]'}`}>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/components/guides/band-scores/band-scores.constants.tsx` around
lines 62 - 69, The map callback is using a hardcoded terminal index (idx === 9)
to toggle the bottom-border in TableRow which will break when the data array
changes; update the mapping logic for the rows (the Array.map that renders
TableRow) to detect the last element dynamically (e.g., use the map signature
.map((row, idx, arr) => ...) and test idx === arr.length - 1 or compute
rows.length - 1) and replace the literal check in the className with that
dynamic condition so the final row always gets 'border-none' regardless of array
size; apply the same pattern to the other tables that use idx === 2/3/4.
apps/user/src/components/guides/speaking-criteria/speaking-criteria.constants.tsx (1)

44-66: ⚖️ Poor tradeoff

Repeated band-table markup — consider extracting a reusable component.

The same overflow-wrapper + Table/TableHeader/TableRow/TableBody structure with identical class strings is repeated for nearly every criterion (Lines 44-66, 73-95, 110-132, 171-193, 200-222, 237-259, 296-326). A small BandTable helper taking a header label and { band, desc }[] rows would remove the duplication and make styling/aria changes a single edit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/user/src/components/guides/speaking-criteria/speaking-criteria.constants.tsx`
around lines 44 - 66, Extract the repeated table block into a small reusable
component named BandTable that accepts props headerLabel: string and rows: {
band: string; desc: string }[] (optionally className); move the common wrapper
div + Table, TableHeader, TableBody and the fixed className strings into
BandTable and implement the map inside BandTable (use row.band as key and
preserve the idx-based class logic for the last row border), then replace each
duplicated block with <BandTable headerLabel="Description of Fluency &
Coherence" rows={...} /> (or the appropriate header text) so styling and aria
changes can be made in one place; ensure the BandTable exports and imports are
updated where the duplicated blocks were removed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/user/src/components/guides/band-scores/band-scores-content.tsx`:
- Around line 14-15: GUIDE_SECTIONS.map(...) is recomputing a new sectionIds
array each render which forces useScrollSpy to re-run its effect; wrap the
mapping in React's useMemo so sectionIds reference is stable (e.g., const
sectionIds = useMemo(() => GUIDE_SECTIONS.map(s => s.id), [GUIDE_SECTIONS]) )
and add an import for useMemo from 'react' so useScrollSpy receives a stable ids
argument and doesn't re-attach the scroll listener unnecessarily.

In
`@apps/user/src/components/guides/listening-reading-scoring/listening-reading-scoring-sidebar.tsx`:
- Around line 20-33: The handleClick handler currently calls e.preventDefault()
unconditionally which blocks hash updates and modifier-click/default navigation;
update handleClick to only intercept plain left-clicks (check e.button === 0 and
no e.metaKey/e.ctrlKey/e.shiftKey/e.altKey) and return early for other clicks so
native behavior (open in new tab, back/forward, etc.) remains; when
intercepting, preventDefault(), perform the smooth scroll to the element (as in
the existing element/offset calculation), and then update the URL hash (e.g.,
via history.pushState or setting location.hash) after scrolling to preserve
navigation/restoration semantics.

In
`@apps/user/src/components/guides/speaking-criteria/speaking-criteria-content.tsx`:
- Around line 14-15: GUIDE_SECTIONS.map(...) is recreating sectionIds every
render which forces useScrollSpy(ids) to re-run its effect; stabilize the
reference by importing useMemo from react and replace the inline map with a
memoized value (e.g., const sectionIds = useMemo(() => GUIDE_SECTIONS.map(s =>
s.id), [GUIDE_SECTIONS])) then pass that memoized sectionIds into useScrollSpy
to avoid repeatedly adding/removing the scroll listener.

In `@apps/user/src/components/guides/test-format/test-format.constants.tsx`:
- Line 341: The displayed total core test duration string "Total Core Test Time:
2 hours 44 minutes (Listening + Reading + Writing) + Speaking" is incorrect;
update that literal to "Total Core Test Time: 2 hours 40 minutes (Listening +
Reading + Writing) + Speaking" in the constant located in test-format.constants
(search for the exact string to find it) so the summary matches the summed
paper-based durations shown in the same section.

In
`@apps/user/src/components/guides/writing-criteria/writing-criteria.constants.tsx`:
- Line 62: Remove the hardcoded numeric prefixes from the title properties in
apps/user/src/components/guides/writing-criteria/writing-criteria.constants.tsx
— update the objects whose title strings start with "1. ", "2. ", "3. ", "4. "
(the entries for Task Achievement / Task Response, Coherence and Cohesion,
Lexical Resource, and Grammatical Range and Accuracy) so the title values no
longer include leading numbers, since the renderer already prefixes headings
with section.number.

---

Nitpick comments:
In `@apps/user/src/components/guides/band-scores/band-scores.constants.tsx`:
- Around line 62-69: The map callback is using a hardcoded terminal index (idx
=== 9) to toggle the bottom-border in TableRow which will break when the data
array changes; update the mapping logic for the rows (the Array.map that renders
TableRow) to detect the last element dynamically (e.g., use the map signature
.map((row, idx, arr) => ...) and test idx === arr.length - 1 or compute
rows.length - 1) and replace the literal check in the className with that
dynamic condition so the final row always gets 'border-none' regardless of array
size; apply the same pattern to the other tables that use idx === 2/3/4.

In
`@apps/user/src/components/guides/listening-reading-scoring/listening-reading-scoring-sidebar.tsx`:
- Around line 19-70: The ListeningReadingScoringSidebar duplicates common
sidebar logic (click-to-scroll, layout and styles); extract a reusable
GuideSidebar component and replace ListeningReadingScoringSidebar to use it:
move the click handler logic (handleClick) and scroll offset calculation into
the new GuideSidebar, accept props for sections (array of {id, number, title}),
activeId, and an optional renderItem or children slot for item content, and keep
the same classNames/structure so existing behavior and styling (smooth scroll,
offset, active class handling) remain unchanged; update
ListeningReadingScoringSidebar to delegate rendering of the nav/ul to
GuideSidebar and pass through sections and activeId.

In
`@apps/user/src/components/guides/speaking-criteria/speaking-criteria-sections.tsx`:
- Around line 18-71: This file duplicates section/hero scaffolding and animation
constants (SECTION_VARIANTS, TRANSITION_CONFIG) that other components (e.g.,
listening-reading-scoring-sections and other *-sections/*-hero files) share;
refactor by extracting the animation constants into a shared module and creating
a generic component (e.g., GuideSections and GuideHero) that accepts readonly
GuideSectionData[] (GUIDE_SECTIONS) and renders the same structure, then replace
SpeakingCriteriaSections to import and use the generic GuideSections with its
specific GuideSectionData type and content; ensure unique IDs/aria labels
(heading-${section.id}) and props (sections) remain supported.

In
`@apps/user/src/components/guides/speaking-criteria/speaking-criteria.constants.tsx`:
- Around line 44-66: Extract the repeated table block into a small reusable
component named BandTable that accepts props headerLabel: string and rows: {
band: string; desc: string }[] (optionally className); move the common wrapper
div + Table, TableHeader, TableBody and the fixed className strings into
BandTable and implement the map inside BandTable (use row.band as key and
preserve the idx-based class logic for the last row border), then replace each
duplicated block with <BandTable headerLabel="Description of Fluency &
Coherence" rows={...} /> (or the appropriate header text) so styling and aria
changes can be made in one place; ensure the BandTable exports and imports are
updated where the duplicated blocks were removed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: af656a4e-879c-41aa-8239-0adc0c51bf87

📥 Commits

Reviewing files that changed from the base of the PR and between 3f646ab and e23c2fc.

📒 Files selected for processing (36)
  • apps/user/src/app/(main)/guides/academic-vs-general/page.tsx
  • apps/user/src/app/(main)/guides/band-scores/page.tsx
  • apps/user/src/app/(main)/guides/listening-reading-scoring/page.tsx
  • apps/user/src/app/(main)/guides/speaking-criteria/page.tsx
  • apps/user/src/app/(main)/guides/test-format/page.tsx
  • apps/user/src/app/(main)/guides/writing-criteria/page.tsx
  • apps/user/src/components/guides/academic-vs-general/academic-vs-general-content.tsx
  • apps/user/src/components/guides/academic-vs-general/academic-vs-general-hero.tsx
  • apps/user/src/components/guides/academic-vs-general/academic-vs-general-sections.tsx
  • apps/user/src/components/guides/academic-vs-general/academic-vs-general-sidebar.tsx
  • apps/user/src/components/guides/academic-vs-general/academic-vs-general.constants.tsx
  • apps/user/src/components/guides/band-scores/band-scores-content.tsx
  • apps/user/src/components/guides/band-scores/band-scores-hero.tsx
  • apps/user/src/components/guides/band-scores/band-scores-sections.tsx
  • apps/user/src/components/guides/band-scores/band-scores-sidebar.tsx
  • apps/user/src/components/guides/band-scores/band-scores.constants.tsx
  • apps/user/src/components/guides/listening-reading-scoring/listening-reading-scoring-content.tsx
  • apps/user/src/components/guides/listening-reading-scoring/listening-reading-scoring-hero.tsx
  • apps/user/src/components/guides/listening-reading-scoring/listening-reading-scoring-sections.tsx
  • apps/user/src/components/guides/listening-reading-scoring/listening-reading-scoring-sidebar.tsx
  • apps/user/src/components/guides/listening-reading-scoring/listening-reading-scoring.constants.tsx
  • apps/user/src/components/guides/speaking-criteria/speaking-criteria-content.tsx
  • apps/user/src/components/guides/speaking-criteria/speaking-criteria-hero.tsx
  • apps/user/src/components/guides/speaking-criteria/speaking-criteria-sections.tsx
  • apps/user/src/components/guides/speaking-criteria/speaking-criteria-sidebar.tsx
  • apps/user/src/components/guides/speaking-criteria/speaking-criteria.constants.tsx
  • apps/user/src/components/guides/test-format/test-format-content.tsx
  • apps/user/src/components/guides/test-format/test-format-hero.tsx
  • apps/user/src/components/guides/test-format/test-format-sections.tsx
  • apps/user/src/components/guides/test-format/test-format-sidebar.tsx
  • apps/user/src/components/guides/test-format/test-format.constants.tsx
  • apps/user/src/components/guides/writing-criteria/writing-criteria-content.tsx
  • apps/user/src/components/guides/writing-criteria/writing-criteria-hero.tsx
  • apps/user/src/components/guides/writing-criteria/writing-criteria-sections.tsx
  • apps/user/src/components/guides/writing-criteria/writing-criteria-sidebar.tsx
  • apps/user/src/components/guides/writing-criteria/writing-criteria.constants.tsx
✅ Files skipped from review due to trivial changes (3)
  • apps/user/src/app/(main)/guides/speaking-criteria/page.tsx
  • apps/user/src/app/(main)/guides/listening-reading-scoring/page.tsx
  • apps/user/src/app/(main)/guides/writing-criteria/page.tsx

Comment thread apps/user/src/components/guides/band-scores/band-scores-content.tsx Outdated
Comment thread apps/user/src/components/guides/speaking-criteria/speaking-criteria-content.tsx Outdated
Comment thread apps/user/src/components/guides/test-format/test-format.constants.tsx Outdated
Comment thread apps/user/src/components/guides/writing-criteria/writing-criteria.constants.tsx Outdated
…ced repetition

- Implement Our Methodology guide with detailed sections, sidebar, and hero components
- Add Spaced Repetition guide explaining cognitive science and memory optimization
- Introduce Vocabulary Strategy guide covering strategic IELTS vocabulary learning
- Create table-driven content with smooth scrollspy navigation for each guide
- Enhance feedback form UI with responsive spacing and improved rating display
- Update footer links to add study tips and external IELTS preparation resources
- Use framer-motion for animated guide section transitions and headers
- Apply consistent styling and accessibility improvements across new guide pages
- Introduce shared SECTION_IDS constant in guide and privacy content components
- Replace individual map calls with shared SECTION_IDS for scroll spy hooks
- Rename CSS keyframes and animation classes to kebab-case for consistency
- Replace font-family strings with unquoted Geist for consistency
- Update ReviewCard and not-found components to use Button asChild wrapping Link for semantics
- Refactor contact and feedback categories to separate gradient, text, and border color properties
- Adjust table row mapping to correctly remove border on last element in multiple guides constants
- Correct total core test time in test-format constants from 2h44m to 2h40m

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (6)
apps/user/src/components/ui/scrollspy-sidebar.tsx (2)

43-43: 💤 Low value

Make the scroll offset configurable to reduce coupling.

The hard-coded 140 offset creates tight coupling with the scroll-spy implementation. If the header height or offset needs to change, this value must be updated in multiple places.

Consider adding an optional scrollOffset prop with 140 as the default:

♻️ Proposed refactor to make offset configurable
 interface ScrollspySidebarProps {
   readonly sections: readonly SidebarSection[];
   readonly activeId: string;
   readonly ariaLabel?: string;
   readonly title?: string;
+  readonly scrollOffset?: number;
 }

 export function ScrollspySidebar({
   sections,
   activeId,
   ariaLabel = 'Table of Contents',
   title = 'Table of Contents',
+  scrollOffset = 140,
 }: ScrollspySidebarProps) {
   const handleClick = (e: MouseEvent<HTMLAnchorElement>, id: string) => {
     // ... 
     e.preventDefault();
     const element = document.getElementById(id);
     if (element) {
-      const offset = 140; // Matches scrollspy offset (140)
+      const offset = scrollOffset;
       const elementPosition = element.getBoundingClientRect().top + window.scrollY;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/components/ui/scrollspy-sidebar.tsx` at line 43, The hard-coded
offset (const offset = 140) in the ScrollspySidebar component couples it to the
scroll-spy/header height; add an optional prop (e.g., scrollOffset?: number) to
the ScrollspySidebar component with a default value of 140 and replace the
inline const offset = 140 usage with that prop (e.g., const offset =
props.scrollOffset ?? 140) so callers can override the offset when header or
layout changes; update any internal uses of the offset inside ScrollspySidebar
(the variable currently named offset) to reference the new prop and update the
component's props/type declaration accordingly.

53-55: 💤 Low value

Remove redundant runtime environment check.

The typeof window !== 'undefined' && window.history check is unnecessary in a client component (marked with 'use client'). Client components only execute in the browser environment where window is always defined.

♻️ Simplify the history update
-      // Safely update the URL hash
-      if (typeof window !== 'undefined' && window.history) {
-        window.history.pushState(null, '', `#${id}`);
-      }
+      // Update the URL hash
+      window.history.pushState(null, '', `#${id}`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/components/ui/scrollspy-sidebar.tsx` around lines 53 - 55,
Remove the redundant runtime check around window in the ScrollspySidebar client
component: replace the if block that checks "typeof window !== 'undefined' &&
window.history" with a direct call to window.history.pushState(null, '',
`#${id}`) (keeping the same id variable) since client components run only in the
browser and window is always defined.
apps/user/src/components/ReviewCard.tsx (1)

70-78: 💤 Low value

Consider removing redundant accessibility attributes on disabled button.

The disabled prop on the Button component should automatically handle aria-disabled state and keyboard focus management. The explicit aria-disabled="true" and tabIndex={-1} attributes are likely redundant if the @ielts/ui Button component properly implements the disabled state.

♻️ Simplified disabled button
 <Button
   size="lg"
   disabled
-  aria-disabled="true"
-  tabIndex={-1}
   className="w-full sm:w-auto font-semibold shadow-md transition-all duration-300 bg-emerald-600/30 text-emerald-400 border border-emerald-500/30 cursor-not-allowed"
 >
   Review Ahead
 </Button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/components/ReviewCard.tsx` around lines 70 - 78, The Button
component is already rendered with disabled, so remove the redundant explicit
accessibility attributes `aria-disabled="true"` and `tabIndex={-1}` from the
ReviewCard JSX to let `Button` (from `@ielts/ui`) manage aria state and focus;
keep the `disabled` prop and any visual classes (e.g., cursor-not-allowed) if
you want the same styling, and verify behavior in the `ReviewCard` component
after the change.
apps/user/src/components/guides/vocabulary-strategy/vocabulary-strategy-sections.tsx (1)

39-39: ⚡ Quick win

Scroll-spy offset is consistent; scroll-mt mismatch likely doesn’t affect active highlighting

  • ScrollspySidebar.handleClick scrolls with offset = 140, and useScrollSpy also defaults to offset = 140, so the active section highlighting should align with the sidebar smooth-scroll behavior.
  • The scroll-mt-28 sm:scroll-mt-32 (112/128) classes mainly affect CSS scroll-margin-top for native hash/scrollIntoView; consider aligning them with 140 if native hash navigation is expected to match the sidebar behavior.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/user/src/components/guides/vocabulary-strategy/vocabulary-strategy-sections.tsx`
at line 39, The CSS scroll-margin-top classes ("scroll-mt-28 sm:scroll-mt-32")
don't match the 140px offset used by ScrollspySidebar.handleClick and
useScrollSpy; update the element's className to use an explicit 140px margin
(e.g., replace "scroll-mt-28 sm:scroll-mt-32" with "scroll-mt-[140px]
sm:scroll-mt-[140px]" or equivalent Tailwind arbitrary value) so native
hash/scrollIntoView behavior aligns with the 140px offset used by useScrollSpy
and ScrollspySidebar.handleClick.
apps/user/src/features/landing/components/hero-section.tsx (2)

264-276: ⚡ Quick win

Derive the dropdown options from CATEGORY_PRESETS instead of hardcoding.

The list ['Academic Words', 'General Training', 'High-Band Idioms'] duplicates the keys already defined in CATEGORY_PRESETS. Adding/renaming a preset would require edits in two places (and the default 'Academic Words' is also repeated on lines 55 and 87).

♻️ Proposed refactor
+const DEFAULT_CATEGORY = 'Academic Words';
+const CATEGORY_NAMES = Object.keys(CATEGORY_PRESETS);
-                        {['Academic Words', 'General Training', 'High-Band Idioms'].map((cat) => (
+                        {CATEGORY_NAMES.map((cat) => (

Then use DEFAULT_CATEGORY in the useState initializer (line 55) and handleResetCard (line 87).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/features/landing/components/hero-section.tsx` around lines 264
- 276, Replace the hardcoded dropdown options with values derived from
CATEGORY_PRESETS and use DEFAULT_CATEGORY for defaults: update the map over
['Academic Words', 'General Training', 'High-Band Idioms'] to iterate
CATEGORY_PRESETS (or Object.keys/CATEGORY_PRESETS.values as appropriate) when
rendering the Button options, and ensure the initial useState (where
DEFAULT_CATEGORY is referenced) and the handleResetCard logic also use
DEFAULT_CATEGORY instead of repeating the literal string; keep existing handlers
setSelectedCategory(cat) and setIsDropdownOpen(false) as-is so behavior is
unchanged.

368-368: ⚡ Quick win

Don’t flag "{currentWord.example}" for react/no-unescaped-entities: this repo’s linting runs Biome (biome check) and no ESLint config/rule mentions were found.

  • In apps/user/src/features/landing/components/hero-section.tsx, category names + default "Academic Words" are duplicated (initial state, reset, and dropdown map). Derive them from CATEGORY_PRESETS to avoid drift.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/user/src/features/landing/components/hero-section.tsx` at line 368, Do
not treat the JSX string "{currentWord.example}" as an ESLint issue; instead,
remove the duplicated hard-coded category names and default value by deriving
them from the existing CATEGORY_PRESETS constant: update the component state
initialiser, the reset handler, and the dropdown/map generation to compute the
default label (formerly "Academic Words") and the list of category names from
CATEGORY_PRESETS so all three locations reference the same source of truth (look
for CATEGORY_PRESETS, the state initialisation for selectedCategory, the reset
function, and the code that maps categories into the dropdown).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/user/src/components/guides/spaced-repetition/spaced-repetition-sections.tsx`:
- Around line 44-47: The transition delay currently sets a fixed 0.05s for every
section except the first (transition={{ ...TRANSITION_CONFIG, delay: index === 0
? 0 : 0.05 }}), which doesn't produce a progressive stagger; update the
transition logic used where you pass transition (and whileInView) to instead
compute a progressive delay such as delay: index * 0.05 (or remove the delay
entirely if no stagger is desired) so that each successive section (using the
index variable and TRANSITION_CONFIG) is delayed progressively as it enters
view.

In `@apps/user/src/features/word-list/components/delete-confirmation-modal.tsx`:
- Around line 40-52: The modal allows Escape and overlay clicks to call onClose
even while a delete mutation is in flight (isPending), which can dismiss the UI
mid-mutation; update the useEffect/handleKeyDown logic and the overlay onClick
to guard against closing when isPending is true: only add the keydown listener
when isOpen && !isPending (or have handleKeyDown return without calling onClose
if isPending), and modify the overlay's onClick handler to ignore clicks when
isPending, ensuring onClose is not invoked while a delete is pending; reference
useEffect, handleKeyDown, isOpen, isPending, onClose and the overlay onClick in
delete-confirmation-modal.tsx.

---

Nitpick comments:
In
`@apps/user/src/components/guides/vocabulary-strategy/vocabulary-strategy-sections.tsx`:
- Line 39: The CSS scroll-margin-top classes ("scroll-mt-28 sm:scroll-mt-32")
don't match the 140px offset used by ScrollspySidebar.handleClick and
useScrollSpy; update the element's className to use an explicit 140px margin
(e.g., replace "scroll-mt-28 sm:scroll-mt-32" with "scroll-mt-[140px]
sm:scroll-mt-[140px]" or equivalent Tailwind arbitrary value) so native
hash/scrollIntoView behavior aligns with the 140px offset used by useScrollSpy
and ScrollspySidebar.handleClick.

In `@apps/user/src/components/ReviewCard.tsx`:
- Around line 70-78: The Button component is already rendered with disabled, so
remove the redundant explicit accessibility attributes `aria-disabled="true"`
and `tabIndex={-1}` from the ReviewCard JSX to let `Button` (from `@ielts/ui`)
manage aria state and focus; keep the `disabled` prop and any visual classes
(e.g., cursor-not-allowed) if you want the same styling, and verify behavior in
the `ReviewCard` component after the change.

In `@apps/user/src/components/ui/scrollspy-sidebar.tsx`:
- Line 43: The hard-coded offset (const offset = 140) in the ScrollspySidebar
component couples it to the scroll-spy/header height; add an optional prop
(e.g., scrollOffset?: number) to the ScrollspySidebar component with a default
value of 140 and replace the inline const offset = 140 usage with that prop
(e.g., const offset = props.scrollOffset ?? 140) so callers can override the
offset when header or layout changes; update any internal uses of the offset
inside ScrollspySidebar (the variable currently named offset) to reference the
new prop and update the component's props/type declaration accordingly.
- Around line 53-55: Remove the redundant runtime check around window in the
ScrollspySidebar client component: replace the if block that checks "typeof
window !== 'undefined' && window.history" with a direct call to
window.history.pushState(null, '', `#${id}`) (keeping the same id variable)
since client components run only in the browser and window is always defined.

In `@apps/user/src/features/landing/components/hero-section.tsx`:
- Around line 264-276: Replace the hardcoded dropdown options with values
derived from CATEGORY_PRESETS and use DEFAULT_CATEGORY for defaults: update the
map over ['Academic Words', 'General Training', 'High-Band Idioms'] to iterate
CATEGORY_PRESETS (or Object.keys/CATEGORY_PRESETS.values as appropriate) when
rendering the Button options, and ensure the initial useState (where
DEFAULT_CATEGORY is referenced) and the handleResetCard logic also use
DEFAULT_CATEGORY instead of repeating the literal string; keep existing handlers
setSelectedCategory(cat) and setIsDropdownOpen(false) as-is so behavior is
unchanged.
- Line 368: Do not treat the JSX string "{currentWord.example}" as an ESLint
issue; instead, remove the duplicated hard-coded category names and default
value by deriving them from the existing CATEGORY_PRESETS constant: update the
component state initialiser, the reset handler, and the dropdown/map generation
to compute the default label (formerly "Academic Words") and the list of
category names from CATEGORY_PRESETS so all three locations reference the same
source of truth (look for CATEGORY_PRESETS, the state initialisation for
selectedCategory, the reset function, and the code that maps categories into the
dropdown).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 297d9fdd-6896-413e-a1c5-4c2ff1ee9a30

📥 Commits

Reviewing files that changed from the base of the PR and between 3f646ab and 5311be1.

📒 Files selected for processing (75)
  • apps/backend/src/config/mongo.ts
  • apps/user/src/app/(main)/guides/academic-vs-general/page.tsx
  • apps/user/src/app/(main)/guides/band-scores/page.tsx
  • apps/user/src/app/(main)/guides/listening-reading-scoring/page.tsx
  • apps/user/src/app/(main)/guides/methodology/page.tsx
  • apps/user/src/app/(main)/guides/spaced-repetition/page.tsx
  • apps/user/src/app/(main)/guides/speaking-criteria/page.tsx
  • apps/user/src/app/(main)/guides/test-format/page.tsx
  • apps/user/src/app/(main)/guides/vocabulary-strategy/page.tsx
  • apps/user/src/app/(main)/guides/writing-criteria/page.tsx
  • apps/user/src/app/(main)/not-found.tsx
  • apps/user/src/app/global.css
  • apps/user/src/components/ReviewCard.tsx
  • apps/user/src/components/contact/contact-categories.tsx
  • apps/user/src/components/feedback/feedback-categories.tsx
  • apps/user/src/components/feedback/feedback-form.tsx
  • apps/user/src/components/footer/UserFooter.tsx
  • apps/user/src/components/footer/footer.constants.ts
  • apps/user/src/components/guides/academic-vs-general/academic-vs-general-content.tsx
  • apps/user/src/components/guides/academic-vs-general/academic-vs-general-hero.tsx
  • apps/user/src/components/guides/academic-vs-general/academic-vs-general-sections.tsx
  • apps/user/src/components/guides/academic-vs-general/academic-vs-general.constants.tsx
  • apps/user/src/components/guides/band-scores/band-scores-content.tsx
  • apps/user/src/components/guides/band-scores/band-scores-hero.tsx
  • apps/user/src/components/guides/band-scores/band-scores-sections.tsx
  • apps/user/src/components/guides/band-scores/band-scores.constants.tsx
  • apps/user/src/components/guides/listening-reading-scoring/listening-reading-scoring-content.tsx
  • apps/user/src/components/guides/listening-reading-scoring/listening-reading-scoring-hero.tsx
  • apps/user/src/components/guides/listening-reading-scoring/listening-reading-scoring-sections.tsx
  • apps/user/src/components/guides/listening-reading-scoring/listening-reading-scoring.constants.tsx
  • apps/user/src/components/guides/methodology/methodology-content.tsx
  • apps/user/src/components/guides/methodology/methodology-hero.tsx
  • apps/user/src/components/guides/methodology/methodology-sections.tsx
  • apps/user/src/components/guides/methodology/methodology.constants.tsx
  • apps/user/src/components/guides/spaced-repetition/spaced-repetition-content.tsx
  • apps/user/src/components/guides/spaced-repetition/spaced-repetition-hero.tsx
  • apps/user/src/components/guides/spaced-repetition/spaced-repetition-sections.tsx
  • apps/user/src/components/guides/spaced-repetition/spaced-repetition.constants.tsx
  • apps/user/src/components/guides/speaking-criteria/speaking-criteria-content.tsx
  • apps/user/src/components/guides/speaking-criteria/speaking-criteria-hero.tsx
  • apps/user/src/components/guides/speaking-criteria/speaking-criteria-sections.tsx
  • apps/user/src/components/guides/speaking-criteria/speaking-criteria.constants.tsx
  • apps/user/src/components/guides/test-format/test-format-content.tsx
  • apps/user/src/components/guides/test-format/test-format-hero.tsx
  • apps/user/src/components/guides/test-format/test-format-sections.tsx
  • apps/user/src/components/guides/test-format/test-format.constants.tsx
  • apps/user/src/components/guides/vocabulary-strategy/vocabulary-strategy-content.tsx
  • apps/user/src/components/guides/vocabulary-strategy/vocabulary-strategy-hero.tsx
  • apps/user/src/components/guides/vocabulary-strategy/vocabulary-strategy-sections.tsx
  • apps/user/src/components/guides/vocabulary-strategy/vocabulary-strategy.constants.tsx
  • apps/user/src/components/guides/writing-criteria/writing-criteria-content.tsx
  • apps/user/src/components/guides/writing-criteria/writing-criteria-hero.tsx
  • apps/user/src/components/guides/writing-criteria/writing-criteria-sections.tsx
  • apps/user/src/components/guides/writing-criteria/writing-criteria.constants.tsx
  • apps/user/src/components/privacy/privacy-content.tsx
  • apps/user/src/components/privacy/privacy-hero.tsx
  • apps/user/src/components/terms/terms-content.tsx
  • apps/user/src/components/terms/terms-hero.tsx
  • apps/user/src/components/ui/scrollspy-sidebar.tsx
  • apps/user/src/features/auth/components/forgot-password-form.tsx
  • apps/user/src/features/auth/components/login-form.tsx
  • apps/user/src/features/auth/components/register-form.tsx
  • apps/user/src/features/auth/hooks/use-login.ts
  • apps/user/src/features/landing/components/hero-section.tsx
  • apps/user/src/features/landing/components/testimonials.tsx
  • apps/user/src/features/review/components/flashcard.tsx
  • apps/user/src/features/vocabulary/components/vocabulary-filters.tsx
  • apps/user/src/features/word-list/components/delete-confirmation-modal.tsx
  • apps/user/src/features/word-list/components/list-card.tsx
  • apps/user/src/features/word-list/hooks/use-save-to-list.ts
  • apps/user/src/features/word-list/index.tsx
  • libs/ui/src/components/ui/icons.tsx
  • libs/ui/src/components/ui/toast.tsx
  • libs/ui/src/hooks/use-toast.ts
  • libs/ui/src/lib/WordDetailsModal.tsx
💤 Files with no reviewable changes (1)
  • apps/user/src/features/word-list/components/list-card.tsx
✅ Files skipped from review due to trivial changes (2)
  • apps/user/src/components/guides/speaking-criteria/speaking-criteria-hero.tsx
  • apps/user/src/components/guides/vocabulary-strategy/vocabulary-strategy.constants.tsx
🚧 Files skipped from review as they are similar to previous changes (45)
  • apps/user/src/components/guides/academic-vs-general/academic-vs-general-sections.tsx
  • apps/user/src/components/guides/listening-reading-scoring/listening-reading-scoring-hero.tsx
  • apps/user/src/components/guides/academic-vs-general/academic-vs-general-content.tsx
  • apps/user/src/components/guides/band-scores/band-scores-content.tsx
  • apps/user/src/components/guides/writing-criteria/writing-criteria-content.tsx
  • apps/user/src/app/(main)/guides/listening-reading-scoring/page.tsx
  • apps/user/src/features/auth/components/login-form.tsx
  • apps/user/src/app/(main)/guides/writing-criteria/page.tsx
  • apps/user/src/app/(main)/not-found.tsx
  • apps/user/src/components/guides/band-scores/band-scores-hero.tsx
  • apps/user/src/components/guides/speaking-criteria/speaking-criteria.constants.tsx
  • apps/user/src/components/terms/terms-hero.tsx
  • apps/user/src/components/guides/test-format/test-format-sections.tsx
  • apps/user/src/app/(main)/guides/academic-vs-general/page.tsx
  • apps/user/src/features/auth/components/forgot-password-form.tsx
  • apps/user/src/components/privacy/privacy-content.tsx
  • apps/user/src/components/terms/terms-content.tsx
  • apps/backend/src/config/mongo.ts
  • apps/user/src/components/guides/writing-criteria/writing-criteria-hero.tsx
  • apps/user/src/components/guides/test-format/test-format-content.tsx
  • apps/user/src/components/guides/band-scores/band-scores-sections.tsx
  • apps/user/src/components/guides/academic-vs-general/academic-vs-general.constants.tsx
  • apps/user/src/components/contact/contact-categories.tsx
  • apps/user/src/components/guides/listening-reading-scoring/listening-reading-scoring-content.tsx
  • apps/user/src/components/guides/writing-criteria/writing-criteria-sections.tsx
  • apps/user/src/features/word-list/index.tsx
  • apps/user/src/components/guides/listening-reading-scoring/listening-reading-scoring-sections.tsx
  • apps/user/src/features/landing/components/testimonials.tsx
  • apps/user/src/components/guides/academic-vs-general/academic-vs-general-hero.tsx
  • apps/user/src/app/(main)/guides/speaking-criteria/page.tsx
  • apps/user/src/features/review/components/flashcard.tsx
  • apps/user/src/components/guides/band-scores/band-scores.constants.tsx
  • apps/user/src/components/guides/test-format/test-format.constants.tsx
  • apps/user/src/components/feedback/feedback-form.tsx
  • apps/user/src/components/footer/UserFooter.tsx
  • apps/user/src/features/auth/components/register-form.tsx
  • apps/user/src/components/guides/speaking-criteria/speaking-criteria-content.tsx
  • apps/user/src/components/footer/footer.constants.ts
  • apps/user/src/components/guides/speaking-criteria/speaking-criteria-sections.tsx
  • apps/user/src/components/privacy/privacy-hero.tsx
  • apps/user/src/features/auth/hooks/use-login.ts
  • apps/user/src/components/guides/listening-reading-scoring/listening-reading-scoring.constants.tsx
  • apps/user/src/components/guides/writing-criteria/writing-criteria.constants.tsx
  • apps/user/src/features/word-list/hooks/use-save-to-list.ts
  • apps/user/src/app/global.css

Comment thread apps/user/src/features/word-list/components/delete-confirmation-modal.tsx Outdated
…Table component

- Created BandTable component to render band score tables with narrow and wide variants
- Replaced multiple inline band score tables in speaking-criteria.constants.tsx with BandTable usage
- Simplified table rendering logic for fluency, vocabulary, grammar, and pronunciation criteria
- Improved consistency and maintainability of band score presentation

feat(ui): add scrollOffset prop to ScrollspySidebar for configurable offset

- Introduced scrollOffset prop to allow customizing scroll position offset value
- Changed hardcoded scroll offset from 140 to use scrollOffset prop with default 140
- Updated smooth scroll handler to apply dynamic offset

fix(hero): use dynamic category keys and default for selection

- Initialized default category state from CATEGORY_PRESETS keys instead of hardcoded string
- Rendered category selection buttons dynamically based on CATEGORY_KEYS array
- Updated reset handler to set selected category to default dynamically

fix(delete-confirmation-modal): prevent closing during pending state

- Disabled Escape key handler and backdrop click when isPending is true to avoid premature closing
- Disabled Cancel button while pending to improve UX
- Added isPending dependency in effect cleanup to correctly handle event listeners

style(review-card): remove redundant aria-disabled and tabIndex on disabled button

- Removed unnecessary aria-disabled="true" and tabIndex={-1} from disabled ReviewCard button
- Cleaned up button accessibility attributes, relying on disabled attribute only

fix(guides): adjust section scroll margin and animation delay

- Changed scroll margin top from fixed value classes to a consistent 140px for sections
- Updated transition delay to use index * 0.05 instead of conditional zero or 0.05 for smooth staggered animation
- Removed right: 0 from header class to prevent overlap issues
- Added inline style to set right based on CSS variable for scrollbar size
- Ensured header remains fixed and properly aligned without horizontal overflow
- Preserved existing layout and pointer events functionality
… and styles

- Replace Chakra UI components with custom @ielts/ui components in admin features
- Update Admins page table to use new table components and modernize styling
- Revamp Admin login, forgot password, and not-found pages with new styling and components
- Implement glassmorphism design tokens and utility classes in global CSS for consistent UI
- Add new visual feedback elements like glow effects and gradients
- Simplify layout components by removing Chakra UI providers and replacing with Toaster component
- Update toast notifications to use variant styles instead of status props
- Improve accessibility and structure in forms with proper labels and required attributes
- Add demo mode info banner in dashboard layout for read-only user notification
- Remove unused dependencies and update imports accordingly

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/admin/src/features/dashboard/components/MetricCard.tsx (1)

4-19: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused required color prop from MetricCard

MetricCardProps still requires color: string, but MetricCard never destructures or uses color (only icon is used). Remove color from MetricCardProps, and drop the color="..." prop from the five MetricCard call sites in apps/admin/src/features/dashboard/index.tsx.

♻️ Proposed fix
 interface MetricCardProps {
   label: string;
   sublabel: string;
   value: number | string | undefined;
   change?: number; // Percent change
   icon: LucideIcon;
-  color: string;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/admin/src/features/dashboard/components/MetricCard.tsx` around lines 4 -
19, Remove the unused required color prop from the MetricCard API: edit the
MetricCardProps interface to drop the color: string field and ensure the
MetricCard component signature remains unchanged (it already only destructures
label, sublabel, value, change, and icon as IconComponent). Then update the five
call sites in apps/admin/src/features/dashboard/index.tsx to stop passing
color="..." to <MetricCard ... /> so callers match the new prop type; verify
TypeScript compiles after removing these color props.
🧹 Nitpick comments (2)
apps/admin/src/features/admins/index.tsx (1)

98-105: ⚡ Quick win

Unfinished placeholder on the Create Admin action.

The button invokes alert('Create Admin Modal TODO'), which is a development placeholder. If this button is user-visible in the redesigned UI, it ships a broken action.

Want me to scaffold the Create Admin modal (form + mutation wired to the existing query invalidation) or open a tracking issue?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/admin/src/features/admins/index.tsx` around lines 98 - 105, The Create
Admin button currently calls a dev placeholder alert; replace it with a real
modal flow: add local state (e.g., showCreateAdminModal boolean) toggled by the
Button's onClick, render a new CreateAdminModal component when
showCreateAdminModal is true, wire that modal's submit to the existing
createAdmin mutation (useMutation) and on success close the modal and invalidate
or refetch the admins query (e.g., queryClient.invalidateQueries('admins') or
the existing admins query key) so the list refreshes; reference the Button
onClick, CreateAdminModal component, the createAdmin mutation hook, and the
admins query invalidation when implementing.
apps/admin/src/features/settings/index.tsx (1)

20-64: 💤 Low value

Optional: add ARIA tab semantics for the visual tabs.

These buttons render as a tab bar but expose no tab semantics to assistive tech. Consider role="tablist" on the wrapper, role="tab" + aria-selected on each button, and role="tabpanel" on the rendered section. Functionally fine as-is.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/admin/src/features/settings/index.tsx` around lines 20 - 64, The visual
tab bar currently built with the wrapper div and Button components (using
setActiveTab and activeTab) needs ARIA tab semantics: add role="tablist" to the
wrapper div, give each tab Button role="tab" and set aria-selected={activeTab
=== 'profile'|'security'} plus an id (e.g. "tab-profile"/"tab-security") and
aria-controls pointing to the matching panel id, and mark the conditional
content wrappers that render ProfileForm and SecurityForm with role="tabpanel",
corresponding ids (e.g. "panel-profile"/"panel-security"), and aria-labelledby
that points back to the tab id; ensure only the active panel is visible to
assistive tech (e.g. render aria-hidden or remove inactive panels) so screen
readers see the selected tab and its panel correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/admin/src/app/dashboard/layout.tsx`:
- Around line 29-34: The banner uses text-primary-foreground on the section and
the h4 which reduces contrast against the translucent bg-primary/10; remove the
section-level text-primary-foreground and change the h4's class from
text-primary-foreground to text-foreground (or text-primary if you prefer
stronger tint) so the heading uses the appropriate foreground color; update the
JSX where the <section className="... text-primary-foreground ..."> and the <h4
className="font-semibold text-primary-foreground text-sm"> are defined to
reflect these class changes.

In `@apps/admin/src/features/admins/components/DeleteAdminDialog.tsx`:
- Around line 25-35: The dialog body text currently rendered in a plain div
inside DeleteAdminDialog (within DialogContent/DialogHeader/DialogTitle) should
be rendered using DialogDescription to provide an accessible aria-describedby;
replace the <div className="py-4 text-center text-sm text-muted-foreground
leading-relaxed"> that contains the confirmation and
<strong>{adminName}</strong> with a DialogDescription element (preserving the
className and inner content) so the dialog has an accessible description.

In `@apps/admin/src/features/users/components/BanUserDialog.tsx`:
- Around line 26-36: The dialog content in BanUserDialog renders a plain <div>
description inside DialogContent which isn't connected to assistive tech;
replace or wrap that descriptive <div> with the DialogDescription component (or
add aria-describedby linking to an id) so the dialog's description is
programmatically associated with the dialog; update the component in
BanUserDialog around DialogContent/DialogTitle to use DialogDescription (or
assign an id to the description and reference it from the dialog root) to
restore proper accessibility for screen readers.

In `@apps/admin/src/features/users/components/UserDetailModal.tsx`:
- Around line 24-46: The dialog is missing a DialogDescription (or an
aria-describedby) which Radix expects for accessibility; inside the
UserDetailModal component, wrap the email/sub-header (or a short descriptive
sentence) in a DialogDescription element (give it a stable id) and ensure the
DialogContent (or the root Dialog trigger) references that id via
aria-describedby if DialogDescription is not used; update the
DialogContent/DialogTitle usage so the description is programmatically
associated (use the DialogDescription component or set aria-describedby to the
description id) so screen readers announce the description along with
DialogTitle.

In `@apps/admin/src/features/vocabulary/components/DeleteWordDialog.tsx`:
- Around line 25-35: Change the plain confirmation <div> inside DeleteWordDialog
to use the DialogDescription component so the message is connected via
aria-describedby (update the JSX inside DialogContent where
DialogHeader/DialogTitle are defined), and modify the dialog's onOpenChange
handler (the prop controlling open state) so it prevents closing when isLoading
is true by ignoring onOpenChange events that set open to false while isLoading
is true; keep calling onClose only when not loading. Ensure references to
DialogContent, DialogHeader, DialogTitle, DialogDescription, onOpenChange,
isLoading, and onClose are updated accordingly.

---

Outside diff comments:
In `@apps/admin/src/features/dashboard/components/MetricCard.tsx`:
- Around line 4-19: Remove the unused required color prop from the MetricCard
API: edit the MetricCardProps interface to drop the color: string field and
ensure the MetricCard component signature remains unchanged (it already only
destructures label, sublabel, value, change, and icon as IconComponent). Then
update the five call sites in apps/admin/src/features/dashboard/index.tsx to
stop passing color="..." to <MetricCard ... /> so callers match the new prop
type; verify TypeScript compiles after removing these color props.

---

Nitpick comments:
In `@apps/admin/src/features/admins/index.tsx`:
- Around line 98-105: The Create Admin button currently calls a dev placeholder
alert; replace it with a real modal flow: add local state (e.g.,
showCreateAdminModal boolean) toggled by the Button's onClick, render a new
CreateAdminModal component when showCreateAdminModal is true, wire that modal's
submit to the existing createAdmin mutation (useMutation) and on success close
the modal and invalidate or refetch the admins query (e.g.,
queryClient.invalidateQueries('admins') or the existing admins query key) so the
list refreshes; reference the Button onClick, CreateAdminModal component, the
createAdmin mutation hook, and the admins query invalidation when implementing.

In `@apps/admin/src/features/settings/index.tsx`:
- Around line 20-64: The visual tab bar currently built with the wrapper div and
Button components (using setActiveTab and activeTab) needs ARIA tab semantics:
add role="tablist" to the wrapper div, give each tab Button role="tab" and set
aria-selected={activeTab === 'profile'|'security'} plus an id (e.g.
"tab-profile"/"tab-security") and aria-controls pointing to the matching panel
id, and mark the conditional content wrappers that render ProfileForm and
SecurityForm with role="tabpanel", corresponding ids (e.g.
"panel-profile"/"panel-security"), and aria-labelledby that points back to the
tab id; ensure only the active panel is visible to assistive tech (e.g. render
aria-hidden or remove inactive panels) so screen readers see the selected tab
and its panel correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 61c7ba74-874c-49cd-a172-b0941ec5b3b5

📥 Commits

Reviewing files that changed from the base of the PR and between e00c579 and c8f86c5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (68)
  • apps/admin/components.json
  • apps/admin/package.json
  • apps/admin/src/app/(auth)/forgot-password/page.tsx
  • apps/admin/src/app/(auth)/login/page.tsx
  • apps/admin/src/app/dashboard/layout.tsx
  • apps/admin/src/app/global.css
  • apps/admin/src/app/layout.tsx
  • apps/admin/src/app/not-found.tsx
  • apps/admin/src/app/page.tsx
  • apps/admin/src/features/admins/components/AdminTableSkeleton.tsx
  • apps/admin/src/features/admins/components/DeleteAdminDialog.tsx
  • apps/admin/src/features/admins/index.tsx
  • apps/admin/src/features/dashboard/components/AlertSection.tsx
  • apps/admin/src/features/dashboard/components/DAUTrendChart.tsx
  • apps/admin/src/features/dashboard/components/DashboardSection.tsx
  • apps/admin/src/features/dashboard/components/DifficultyDistributionChart.tsx
  • apps/admin/src/features/dashboard/components/MetricCard.tsx
  • apps/admin/src/features/dashboard/components/ModuleDistributionChart.tsx
  • apps/admin/src/features/dashboard/components/ProblemWordsCard.tsx
  • apps/admin/src/features/dashboard/components/QuickActions.tsx
  • apps/admin/src/features/dashboard/components/StatsGrid.tsx
  • apps/admin/src/features/dashboard/components/TopWordsCard.tsx
  • apps/admin/src/features/dashboard/components/TopicDistributionChart.tsx
  • apps/admin/src/features/dashboard/components/UnusedWordsCard.tsx
  • apps/admin/src/features/dashboard/components/VocabularyOverviewCard.tsx
  • apps/admin/src/features/dashboard/components/dashboard-skeleton.tsx
  • apps/admin/src/features/dashboard/index.tsx
  • apps/admin/src/features/problem-words/components/ProblemWordsTable.tsx
  • apps/admin/src/features/problem-words/components/ProblemWordsTableSkeleton.tsx
  • apps/admin/src/features/problem-words/index.tsx
  • apps/admin/src/features/settings/components/ProfileForm.tsx
  • apps/admin/src/features/settings/components/SecurityForm.tsx
  • apps/admin/src/features/settings/index.tsx
  • apps/admin/src/features/users/components/BanUserDialog.tsx
  • apps/admin/src/features/users/components/UserDetailModal.tsx
  • apps/admin/src/features/users/components/UserHeader.tsx
  • apps/admin/src/features/users/components/UserSearch.tsx
  • apps/admin/src/features/users/components/UserTable.tsx
  • apps/admin/src/features/users/components/UserTableSkeleton.tsx
  • apps/admin/src/features/users/hooks/use-users.ts
  • apps/admin/src/features/users/index.tsx
  • apps/admin/src/features/vocabulary/components/DeleteWordDialog.tsx
  • apps/admin/src/features/vocabulary/components/VocabularyFilters.tsx
  • apps/admin/src/features/vocabulary/components/VocabularyHeader.tsx
  • apps/admin/src/features/vocabulary/components/VocabularyTable.tsx
  • apps/admin/src/features/vocabulary/components/WordForm/BasicInfoFields.tsx
  • apps/admin/src/features/vocabulary/components/WordForm/MetadataFields.tsx
  • apps/admin/src/features/vocabulary/components/WordForm/ModulesField.tsx
  • apps/admin/src/features/vocabulary/components/WordForm/RelatedWordsFields.tsx
  • apps/admin/src/features/vocabulary/components/WordForm/TopicsField.tsx
  • apps/admin/src/features/vocabulary/components/WordModal.tsx
  • apps/admin/src/features/vocabulary/hooks/use-vocabulary.ts
  • apps/admin/src/features/vocabulary/hooks/useTopicAutocomplete.ts
  • apps/admin/src/features/vocabulary/hooks/useWordMutation.ts
  • apps/admin/src/features/vocabulary/index.tsx
  • libs/ui/src/components/DashboardChart.tsx
  • libs/ui/src/components/auth/ResetPasswordForm.tsx
  • libs/ui/src/components/sidebar/Sidebar.tsx
  • libs/ui/src/components/sidebar/SidebarFooter.tsx
  • libs/ui/src/components/sidebar/SidebarItem.tsx
  • libs/ui/src/components/sidebar/index.ts
  • libs/ui/src/components/sidebar/sidebar.config.ts
  • libs/ui/src/components/sidebar/useLogout.ts
  • libs/ui/src/hooks/useViewerRestriction.ts
  • libs/ui/src/index.ts
  • libs/ui/src/providers/chakra-provider.tsx
  • libs/ui/src/theme/index.ts
  • package.json

Comment thread apps/admin/src/app/dashboard/layout.tsx Outdated
Comment thread apps/admin/src/features/admins/components/DeleteAdminDialog.tsx Outdated
Comment thread apps/admin/src/features/users/components/BanUserDialog.tsx Outdated
Comment thread apps/admin/src/features/users/components/UserDetailModal.tsx Outdated
Comment thread apps/admin/src/features/vocabulary/components/DeleteWordDialog.tsx Outdated
…ility

- Replace plain divs with DialogDescription component in various dialogs for semantic clarity
- Add appropriate ARIA roles and attributes to settings tab buttons and panels for better accessibility
- Prevent dialog close when loading in DeleteWordDialog
- Remove unused color prop from MetricCard component
- Adjust UI styles and text colors in dashboard layout for consistency
- Remove explicit color props from MetricCard instances to simplify props passing
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.

Refactor: Redesign the landing page

1 participant