Skip to content

feat(web,shared): Wave 3 frontend structure — D36 dedup UI + D37 error page consolidation + D40 complete i18n#110

Merged
Ardakilic merged 3 commits into
mainfrom
feat/wave3
Jul 6, 2026
Merged

feat(web,shared): Wave 3 frontend structure — D36 dedup UI + D37 error page consolidation + D40 complete i18n#110
Ardakilic merged 3 commits into
mainfrom
feat/wave3

Conversation

@Ardakilic

@Ardakilic Ardakilic commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Wave 3 — Frontend Structure (D36 + D37 + D40)

Wave 3 of the debt roadmap bundles three frontend-structure items that the ROADMAP.md explicitly sequences together because the order matters: D36 dedupes UI before D40 translates it (so each string is touched once), and D37 settles which error pages exist before D40 localizes them.


D36 — Extract duplicated UI

Cluster Before After
HomePage RecipeCard Local fork (missing currentVersion badge row) Imports shared RecipeCard from components/recipe-list/
Ban dialog + mutation Duplicated in AdminUsersPage + AdminUserDetailPage (detail page silently swallowed errors) BanDialog + useBanUser extracted to components/admin/ + hooks/; errors surfaced on both call sites
Section/Field helpers Local copies in RecipeCreatePage AND RecipeEditPage (as EditSection/EditField) Extracted to components/form/; both pages import shared (27 call sites renamed)

New files: BanDialog.tsx, useBanUser.ts, Section.tsx, Field.tsx, form/index.ts, BanDialog.test.tsx (7 tests), useBanUser.test.ts (9 tests)

D37 — Consolidate error pages

  • ErrorPage.tsx is the canonical module (exports NotFoundPage + ServerErrorPage only; base ErrorPage un-exported; ForbiddenPage deleted; <SEOHead noIndex> added to all variants)
  • NotFoundPage.tsx deleted; router imports from ErrorPage.tsx
  • ErrorBoundary delegates 404 → <NotFoundPage />, 5xx → <ServerErrorPage />; generic fallback uses t() for Go Home/Reload/Oops
  • Kills the third copy of the 404 prose string that was duplicated in ErrorBoundary
  • ErrorBoundary.test.tsx created (4 tests); ErrorPage.test.tsx updated

D40 — Complete i18n

  • ~175 new i18n keys added to both en.json and tr.json (BanDialog, compare, verifyEmail, legal, recipe form, 15 admin pages, common vocabulary)
  • Deterministic bidirectional parity test replaces the sampled one-directional property test
  • 4 user-facing pages converted (RecipeComparePage, VerifyEmailPage, PrivacyPage, TermsPage — legal body kept in English with translated notice per design Decision 7)
  • 2 partial recipe pages fully converted (divergent labels normalized: Dose (g), Extraction Time (s) — both pages use the same recipe.form.dose key)
  • 15 admin pages converted to use t() with useTranslation; existing admin.* nav keys wired into AdminLayout; common.* vocabulary reused (no admin.common.* duplication)
  • 18 tr-locale spot-check test files created (each renders under tr locale and asserts a Turkish string appears)
  • VerifyEmailPage log-message drift fixed ('token verification failed''verifyEmail failed' per web-page-logging spec)
  • PrivacyPage + TermsPage add mount/unmount logs (pre-existing gap)

Verification

Gate Result
make check (type-check all workspaces) ✅ 523 files checked, zero errors
make lint ✅ 523 files checked, zero errors
make fmt ✅ Applied (542 files)
make test (all tests via Docker) ✅ 88 web test files (893 tests) + 107 shared tests
Grep gate: admin pages use i18n ✅ No .tsx file lacks useTranslation
Grep gate: no NotFoundPage.tsx imports
Grep gate: no local RecipeCard in pages
Grep gate: no EditSection/EditField
Grep gate: no ForbiddenPage
Grep gate: base ErrorPage not exported
Grep gate: no notFound.* keys
Locale parity (en/tr) ✅ 707 keys each, all string values

Files changed (67)

  • New: 28 files (BanDialog, useBanUser, Section, Field, form/index, ErrorBoundary.test, 18 spot-check tests, OpenSpec change artifacts)
  • Modified: 34 files (ErrorPage, ErrorBoundary, router, HomePage, 22 page i18n conversions, 2 admin test mocks, en.json, tr.json, i18n.test.ts, ROADMAP.md, TECHNICAL_DEBT.md)
  • Deleted: 1 file (NotFoundPage.tsx)

Design decisions

See openspec/changes/wave-3-frontend-structure/design.md for the 11 design decisions, including:

  • Decision 2: Error page visual style — keep emoji + text-primary (composed version)
  • Decision 3: Delete ForbiddenPage (no clean trigger without changing RequireAuth UX)
  • Decision 5: Reuse existing common.* vocabulary (no admin.common.* duplication)
  • Decision 7: Legal pages — translate headers + notice, keep English legal body
  • Decision 8: Do NOT fix pre-existing empty catch blocks (out of scope for i18n)
  • Decision 10: Deterministic bidirectional locale-parity test

OpenSpec change

openspec/changes/wave-3-frontend-structure/ — 65/65 tasks complete, 4 specs (web-shared-components, error-pages, i18n, recipe-list).

Summary by CodeRabbit

  • New Features
    • Added an admin ban/unban flow with a shared ban dialog and hook, plus reusable form primitives for sections and labeled fields.
    • Consolidated recipe card usage on the home page.
    • Expanded i18n across admin, recipe, auth, and legal/privacy/terms pages (including Turkish spot-check coverage).
  • Bug Fixes
    • Route errors now reliably show the correct 404 vs 500 pages, with improved boundary fallback behavior.
    • Ban/unban flows now surface errors consistently.
  • Tests
    • Added/updated component, hook, error-page, and i18n rendering tests.

…r page consolidation + D40 complete i18n (#110)

Wave 3 of the debt roadmap bundles three frontend-structure items that the
ROADMAP.md explicitly sequences together: D36 dedupes UI before D40
translates it, and D37 settles error pages before D40 localizes them.

D36 — Extract duplicated UI:
- HomePage adopts shared RecipeCard from components/recipe-list/ (local
  fork deleted; currentVersion badge row now renders)
- BanDialog component + useBanUser hook extracted to components/admin/ +
  hooks/; AdminUsersPage + AdminUserDetailPage use shared BanDialog
  (detail page silent-swallow bug fixed with error-display element)
- Section/Field form primitives extracted to components/form/; both
  RecipeCreatePage and RecipeEditPage import shared (EditSection/EditField
  deleted, 27 call sites renamed)

D37 — Consolidate error pages:
- ErrorPage.tsx is canonical (exports NotFoundPage + ServerErrorPage only;
  base ErrorPage un-exported; ForbiddenPage deleted; SEOHead noIndex added)
- NotFoundPage.tsx deleted; router imports from ErrorPage.tsx
- ErrorBoundary delegates 404 to NotFoundPage, 5xx to ServerErrorPage;
  generic fallback uses t() for Go Home/Reload/Oops
- ErrorBoundary.test.tsx created (4 tests); ErrorPage.test.tsx updated

D40 — Complete i18n:
- ~175 new i18n keys added to both en.json and tr.json (BanDialog, compare,
  verifyEmail, legal, recipe form, 15 admin pages, common vocabulary)
- Deterministic bidirectional parity test replaces sampled property test
- 4 user-facing pages converted (RecipeCompare, VerifyEmail, Privacy,
  Terms — legal body kept in English with translated notice)
- 2 partial recipe pages fully converted (divergent labels normalized:
  Dose (g), Extraction Time (s))
- 15 admin pages converted to use t() with useTranslation
- 18 tr-locale spot-check test files created
- VerifyEmailPage log-message drift fixed per web-page-logging spec

Verification: make check + make lint + make fmt + make test all pass
(893 web tests + 107 shared tests); all 7 grep gates clean; ROADMAP.md
and TECHNICAL_DEBT.md updated (D36, D37, D40 marked resolved).
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Ardakilic, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 40 seconds

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

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.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 394f1826-7aaa-4541-ab65-d76578630238

📥 Commits

Reviewing files that changed from the base of the PR and between ea3a899 and e18b07e.

📒 Files selected for processing (7)
  • apps/web/src/components/form/Field.tsx
  • apps/web/src/hooks/useBanUser.test.ts
  • apps/web/src/hooks/useBanUser.ts
  • apps/web/src/pages/admin/AdminUserDetailPage.tsx
  • apps/web/src/pages/admin/AdminUsersPage.tsx
  • packages/shared/src/i18n/en.json
  • packages/shared/src/i18n/tr.json
📝 Walkthrough

Walkthrough

This PR consolidates shared frontend UI, rewires error-page handling, and expands i18n coverage across the web app. It also adds tests, locale keys, and Wave 3 documentation updates for the same frontend-structure effort.

Changes

Wave 3 Frontend Structure

Layer / File(s) Summary
Shared RecipeCard
apps/web/src/pages/HomePage.tsx
HomePage imports the shared recipe-list card instead of a local implementation.
Ban dialog and ban hook
apps/web/src/components/admin/BanDialog.tsx, apps/web/src/hooks/useBanUser.ts, apps/web/src/components/admin/BanDialog.test.tsx, apps/web/src/hooks/useBanUser.test.ts
Introduces the shared ban dialog and hook, with tests covering validation, state transitions, and API outcomes.
Admin user ban flows
apps/web/src/pages/admin/AdminUsersPage.tsx, apps/web/src/pages/admin/AdminUserDetailPage.tsx, related tests
Wires the ban dialog and hook into the admin users list and user detail pages, replacing inline ban logic and local copy.
Shared form primitives and recipe pages
apps/web/src/components/form/*, apps/web/src/pages/recipes/RecipeCreatePage.tsx, apps/web/src/pages/recipes/RecipeEditPage.tsx, related tests
Adds shared Section/Field components and uses them in recipe create/edit pages while localizing the forms.
Error page consolidation
apps/web/src/pages/ErrorPage.tsx, apps/web/src/components/ErrorBoundary.tsx, apps/web/src/router.tsx, related tests
Moves route error rendering to canonical error pages, removes ForbiddenPage, and localizes the generic boundary fallback.
i18n keys and page conversions
packages/shared/src/i18n/*, apps/web/src/pages/*
Adds locale keys, tightens parity checks, and converts admin plus user-facing pages to t()-driven rendering with new tests.
Wave 3 docs and status updates
openspec/changes/wave-3-frontend-structure/*, plans/ROADMAP.md, plans/TECHNICAL_DEBT.md
Adds the Wave 3 design/spec/task docs and marks the roadmap and debt items resolved.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AdminUsersPage
  participant useBanUser
  participant BanDialog
  participant adminApi

  AdminUsersPage->>useBanUser: openBanDialog(user)
  useBanUser->>BanDialog: banDialogUser, reason state
  BanDialog->>useBanUser: onConfirm(reason)
  useBanUser->>adminApi: banUser(userId, reason)
  adminApi-->>useBanUser: success/failure
  useBanUser->>AdminUsersPage: onSuccess(userId, isBanned)
  useBanUser->>BanDialog: closeDialog / error state
Loading
sequenceDiagram
  participant Router
  participant RootErrorBoundary
  participant NotFoundPage
  participant ServerErrorPage

  Router->>RootErrorBoundary: route error thrown
  RootErrorBoundary->>RootErrorBoundary: check error.status
  alt status is 404
    RootErrorBoundary->>NotFoundPage: render
  else status >= 500
    RootErrorBoundary->>ServerErrorPage: render
  else non-route error
    RootErrorBoundary->>RootErrorBoundary: render translated generic fallback
  end
Loading

Possibly related PRs

  • Ardakilic/BrewForm#33: Shares the ban-user flow and reason validation area, which aligns with the new BanDialog and useBanUser ban/unban handling.
  • Ardakilic/BrewForm#67: Also touches RootErrorBoundary routing/error handling, which this PR changes for 404 and 5xx responses.
  • Ardakilic/BrewForm#68: Shares the HomePage RecipeCard extraction path, which this PR completes by importing the shared card.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main Wave 3 changes: UI deduplication, error page consolidation, and completing i18n.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/wave3

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Caution

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

⚠️ Outside diff range comments (4)
apps/web/src/pages/admin/AdminAuditLogPage.tsx (1)

60-65: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Entity-filter <option> labels remain hardcoded English.

Per this cohort's own i18n requirement ("Admin pages use t() for all user-visible strings" / "no user-visible literals remain outside t() calls"), Users, Recipes, Equipment, Vendors, Taste Notes, Reports should be translated too.

🌐 Proposed fix
-          <option value='user'>Users</option>
-          <option value='recipe'>Recipes</option>
-          <option value='equipment'>Equipment</option>
-          <option value='vendor'>Vendors</option>
-          <option value='taste_note'>Taste Notes</option>
-          <option value='report'>Reports</option>
+          <option value='user'>{t('admin.audit.entityUser')}</option>
+          <option value='recipe'>{t('admin.audit.entityRecipe')}</option>
+          <option value='equipment'>{t('admin.audit.entityEquipment')}</option>
+          <option value='vendor'>{t('admin.audit.entityVendor')}</option>
+          <option value='taste_note'>{t('admin.audit.entityTasteNote')}</option>
+          <option value='report'>{t('admin.audit.entityReport')}</option>
🤖 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/web/src/pages/admin/AdminAuditLogPage.tsx` around lines 60 - 65, The
entity filter labels in AdminAuditLogPage are still hardcoded English strings;
update the select options in AdminAuditLogPage to use t() for each user-visible
label so the values for Users, Recipes, Equipment, Vendors, Taste Notes, and
Reports are translated consistently with the rest of the admin page.
apps/web/src/pages/admin/AdminCoffeeVarietiesPage.tsx (1)

590-592: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Pagination "Page X of Y" text left untranslated.

This literal string sits between two now-translated pagination buttons (common.previous/common.next) but wasn't converted, leaving an English fragment in the middle of otherwise localized UI.

🤖 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/web/src/pages/admin/AdminCoffeeVarietiesPage.tsx` around lines 590 -
592, The pagination label in AdminCoffeeVarietiesPage remains hardcoded as “Page
X of Y” while the surrounding controls are translated. Update the span in the
pagination footer to use the same i18n approach as the previous/next buttons,
replacing the literal text with a localized string that interpolates page
values. Refer to the pagination markup near the Page {page} of {Math.ceil(total
/ 20)} display and keep the wording consistent with the existing translation
keys used in this component.
apps/web/src/pages/admin/AdminCompatibilityPage.tsx (1)

75-83: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Two table headers left untranslated.

"Compatible" is localized via t('admin.compatibility.compatible'), but the adjacent "Brew Method" and "Equipment Type" headers in the same row remain hardcoded English strings.

🌐 Proposed fix
                   <th className='text-left py-2 px-3' style={{ color: 'var(--text-secondary)' }}>
-                    Brew Method
+                    {t('admin.compatibility.brewMethod')}
                  </th>
                  <th className='text-left py-2 px-3' style={{ color: 'var(--text-secondary)' }}>
-                    Equipment Type
+                    {t('admin.compatibility.equipmentType')}
                  </th>
🤖 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/web/src/pages/admin/AdminCompatibilityPage.tsx` around lines 75 - 83,
The table header row in AdminCompatibilityPage still hardcodes “Brew Method” and
“Equipment Type” instead of using localized strings. Update the header cells in
the same table row as t('admin.compatibility.compatible') to use translation
keys from the existing i18n setup, matching how Compatible is handled, so all
three headers are translated consistently.
apps/web/src/pages/TermsPage.tsx (1)

29-73: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Section body text still hardcoded — page is not fully localized.

Only the six section headings (legal.terms.s1.titles6.title) were converted to t(). Every paragraph body under those headings (e.g. "By using BrewForm, you agree to these terms...", "You are responsible for your account security...", etc.) remains hardcoded English. This contradicts the PR objective stating the terms page was "fully localized" — Turkish users will see a page with translated headings but English body copy throughout.

🌐 Proposed fix (repeat for each section)
         <h2 ...>{t('legal.terms.s1.title')}</h2>
-        <p>
-          By using BrewForm, you agree to these terms. If you don't agree, please don't use the
-          service.
-        </p>
+        <p>{t('legal.terms.s1.body')}</p>

Add corresponding legal.terms.s1.bodys6.body keys to en.json/tr.json.

🤖 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/web/src/pages/TermsPage.tsx` around lines 29 - 73, The terms page is
only partially localized because the section body paragraphs in TermsPage still
contain hardcoded English text while the headings use t(). Update each section
in TermsPage to read the paragraph content from translation keys as well, using
matching symbols like legal.terms.s1.body through legal.terms.s6.body, and add
those keys to the locale files so both English and Turkish render fully
translated content.
🧹 Nitpick comments (9)
openspec/changes/wave-3-frontend-structure/specs/error-pages/spec.md (1)

213-213: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Markdownlint MD037 flagged on this heading.

The error.* / notFound.* wildcard patterns in this heading are likely being misparsed as emphasis markers. Wrapping them in backticks (already done partially) or escaping the asterisk should resolve the lint warning.

🤖 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 `@openspec/changes/wave-3-frontend-structure/specs/error-pages/spec.md` at line
213, The heading text for the error pages requirement is being misparsed by
markdownlint MD037 because the wildcard patterns in the title read like
emphasis. Update the heading in the spec so the `error.*` and `notFound.*`
tokens are treated as code or escaped consistently, keeping the requirement
wording intact while avoiding the lint warning.

Source: Linters/SAST tools

packages/shared/src/i18n/i18n.test.ts (1)

14-24: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unreachable if...throw after expect().toBe().

expect(typeof value).toBe('string') already throws on mismatch, so the subsequent if (typeof value !== 'string') throw ... is dead code.

♻️ Simplify
   it('every locale value is a string', () => {
     for (const [key, value] of Object.entries(enJson)) {
-      expect(typeof value).toBe('string');
-      if (typeof value !== 'string') throw new Error(`en.json key ${key} is not a string`);
+      expect(typeof value, `en.json key ${key} is not a string`).toBe('string');
     }
     for (const [key, value] of Object.entries(trJson)) {
-      expect(typeof value).toBe('string');
-      if (typeof value !== 'string') throw new Error(`tr.json key ${key} is not a string`);
+      expect(typeof value, `tr.json key ${key} is not a string`).toBe('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 `@packages/shared/src/i18n/i18n.test.ts` around lines 14 - 24, The `every
locale value is a string` test in `i18n.test.ts` has redundant dead code because
`expect(typeof value).toBe('string')` already fails on non-strings. Remove the
following `if (typeof value !== 'string') throw ...` checks from both the
`enJson` and `trJson` loops, and keep the test using the `Object.entries`
iteration with the existing `expect` assertions only.
openspec/changes/wave-3-frontend-structure/specs/i18n/spec.md (1)

56-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Documented key-naming convention doesn't match the delivered keys.

The spec prescribes recipe.create.section.<name>, recipe.create.field.<name>, compare.<field>, etc., but the actual en.json/tr.json additions use recipe.createPage.heading, recipe.form.basicInfo, recipe.editPage.*, compare.notFound, compare.seoTitle. Worth reconciling the spec text with what was actually shipped so it stays a reliable reference.

🤖 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 `@openspec/changes/wave-3-frontend-structure/specs/i18n/spec.md` around lines
56 - 66, The i18n key-naming rules in the spec do not match the keys that were
actually delivered, so reconcile the documented convention with the shipped
translations. Update the spec in i18n/spec.md to reflect the real naming used by
the RecipeCreatePage, RecipeEditPage, RecipeComparePage, and related entries
(for example the createPage/editPage/form-style keys and
compare.notFound/compare.seoTitle), or adjust the delivered keys to match the
spec, but make them consistent. Use the existing section that lists D40 key
conventions and the referenced page-specific symbols to align the wording.
apps/web/src/pages/admin/AdminCoffeeVarietiesPage.tsx (1)

515-525: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Category badge shows raw enum value, not the translated label.

The category filter/select uses t('admin.coffeeVarieties.catVariety') etc., but the table row badge renders the raw item.category value (variety/processing/market_name), which is inconsistent with the rest of the now-localized 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/web/src/pages/admin/AdminCoffeeVarietiesPage.tsx` around lines 515 -
525, The category badge in AdminCoffeeVarietiesPage is rendering the raw enum
value from item.category instead of the localized label used elsewhere on the
page. Update the table row badge rendering to map each category value to the
corresponding translation key (the same ones used by the category filter/select,
such as t('admin.coffeeVarieties.catVariety'),
t('admin.coffeeVarieties.catProcessing'), and
t('admin.coffeeVarieties.catMarketName')) before displaying it, while keeping
CATEGORY_BADGE_COLORS keyed by the underlying category value.
apps/web/src/pages/admin/AdminUserEditPage.tsx (1)

244-251: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Reused auth.resetPassword.newPassword key may carry wrong context/tone.

This admin-edit-any-user form reuses auth.resetPassword.newPassword, which is presumably authored for the self-service password-reset flow (e.g. "Your new password"). If the underlying string uses second-person/possessive phrasing, it will read oddly here where an admin is setting another user's password. Consider a dedicated admin.users.newPassword key instead of sharing the reset-password 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/web/src/pages/admin/AdminUserEditPage.tsx` around lines 244 - 251, The
password label in AdminUserEditPage is reusing the self-service reset key, which
can give the wrong tone/context for an admin editing another user. Update the
label to use a dedicated admin-specific translation key instead of
t('auth.resetPassword.newPassword'), and add the matching key under the admin
users copy so the AdminUserEditPage form reads naturally in this flow.
apps/web/src/pages/admin/AdminUsersPage.test.tsx (1)

16-18: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

No test coverage for the ban confirm/unban flow.

BanDialog is stubbed to () => null and adminApi only mocks getUsers/toggleAdmin — there's no test exercising openBanDialog → confirm → adminApi.banUser or unbanadminApi.unbanUser. This is exactly the path with the critical reason-forwarding bug flagged in AdminUsersPage.tsx (Lines 31-41, 319-326); a test asserting banUser is called with the typed reason would have caught it.

Also applies to: 60-65, 77-95

🤖 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/web/src/pages/admin/AdminUsersPage.test.tsx` around lines 16 - 18, Add
coverage for the ban/unban confirmation flow in AdminUsersPage tests instead of
stubbing BanDialog to a no-op. Update the AdminUsersPage.test setup to render or
simulate the BanDialog path so the openBanDialog → confirm → adminApi.banUser
and unban → adminApi.unbanUser interactions are exercised. Use the existing
AdminUsersPage, BanDialog, and adminApi mocks to assert the typed reason is
forwarded into banUser, and verify the unban path still calls unbanUser
correctly.
apps/web/src/components/admin/BanDialog.tsx (1)

28-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move debug log out of render body.

log.debug at Line 34 runs on every render, including every keystroke in the reason textarea (since setReason on Line 56 triggers a re-render). This produces log spam disproportionate to its diagnostic value.

♻️ Proposed fix: log only on open/close transitions
 export function BanDialog({ user, open, onClose, onConfirm, processing }: BanDialogProps) {
   const { t } = useTranslation();
   const [reason, setReason] = useState('');
 
-  if (!open) return null;
+  useEffect(() => {
+    if (open) log.debug({ userId: user.id }, 'BanDialog opened');
+  }, [open, user.id]);
 
-  log.debug({ userId: user.id, open }, 'BanDialog render');
+  if (!open) return null;
🤖 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/web/src/components/admin/BanDialog.tsx` around lines 28 - 35, The debug
logging in BanDialog is inside the render path and fires on every re-render,
including each reason input change. Move the log.debug call out of the component
body and into a useEffect tied to the open state in BanDialog so it only logs on
open/close transitions, using the existing BanDialogProps, user.id, and open
values to keep the same context without render-time spam.
apps/web/src/hooks/useBanUser.ts (1)

84-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated error-extraction + untranslated fallback strings.

Both confirmBan and unban repeat (err as { message?: string })?.message || '...', and the fallback text is hardcoded English ('Failed to ban user.' / 'Failed to unban user.'), inconsistent with this PR's i18n completion effort — this error is rendered directly in banError at the call sites.

♻️ Extract a shared helper and translate fallback text
+function getErrorMessage(err: unknown, fallback: string): string {
+  return (err as { message?: string })?.message || fallback;
+}

Then use getErrorMessage(err, t('admin.users.banFailed')) (via useTranslation() in the hook) at both call sites.

Also applies to: 99-99

🤖 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/web/src/hooks/useBanUser.ts` at line 84, The error handling in
useBanUser duplicates the same message extraction in both confirmBan and unban
and still falls back to hardcoded English text. Add a shared helper in
useBanUser to derive the message from err, then replace both inline expressions
with that helper and pass translated fallback strings from useTranslation() (for
example via t('admin.users.banFailed') and the corresponding unban key) so
banError receives localized messages consistently.
apps/web/src/hooks/useBanUser.test.ts (1)

1-161: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Solid coverage of the hook's own state machine (open/confirm success & failure/unban/clearError/closeDialog). Note: once confirmBan's signature changes to accept reason directly (see critical comment in useBanUser.ts), these tests will need updating to call confirmBan('Spam') instead of relying on setReason.

🤖 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/web/src/hooks/useBanUser.test.ts` around lines 1 - 161, The `useBanUser`
tests still rely on `setReason` before calling `confirmBan`, but `confirmBan`
will now take the reason directly. Update the `useBanUser` test cases that cover
confirm-ban success, failure, and empty-reason behavior to call
`confirmBan('Spam')` or `confirmBan('')` on the hook instance instead of setting
state via `setReason`, and keep the assertions around `banDialogUser`,
`processing`, `error`, and `onSuccess` aligned with the new `confirmBan` flow.
🤖 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/web/src/components/form/Field.tsx`:
- Around line 14-24: The Field component’s label is not associated with the
rendered control, so update Field in Field.tsx to use an accessible label/input
pairing. Either wrap the single child inside the <label> in Field, or add an
explicit id/htmlFor contract to FieldProps and pass it through so the label in
Field correctly targets the child control.

In `@apps/web/src/pages/admin/AdminCachePage.tsx`:
- Around line 45-48: The AdminCachePage copy is only partially localized because
the descriptive body text in the cache warning and Deno KV explanation is still
hardcoded English. Update the paragraph content in AdminCachePage to use the
same i18n pattern as the headings/buttons, wiring both remaining body paragraphs
through t(...) (or equivalent translation keys) so all user-facing text on the
page is translated consistently.
- Around line 26-29: The AdminCachePage message styling is still inferring
success/error from the localized message text, so non-English failures can
render with the wrong color. Update the message state and rendering logic in
AdminCachePage to track an explicit status flag for the flush result instead of
checking message contents with string matching. Use the existing flushAll
success/error paths and the message rendering branch together so the color is
driven by the stored status, not by t('admin.cache.flushSuccess') or
t('admin.cache.flushError') text.

In `@apps/web/src/pages/admin/AdminUserDetailPage.tsx`:
- Around line 24-34: Update the useBanUser/BanDialog contract so confirmBan
accepts the ban reason directly instead of relying on internal state that
AdminUserDetailPage does not pass through. Fix the hook in useBanUser.ts by
threading reason into confirmBan and using it when submitting the ban, and keep
AdminUserDetailPage’s onConfirm wiring as-is since it already passes confirmBan
directly. Ensure the hook’s returned API matches the BanDialog expectations,
including any reason-related state or setters that are no longer needed.

In `@apps/web/src/pages/auth/VerifyEmailPage.tsx`:
- Around line 42-47: The error handling in VerifyEmailPage’s verification flow
is using the raw backend message instead of a localized user-facing string.
Update the catch block in VerifyEmailPage so that setErrorMessage always uses
the translated fallback from t('verifyEmail.failed') and does not read
err.message for display, while still keeping the existing log.error call for
debugging context.

In `@apps/web/src/pages/ErrorPage.test.tsx`:
- Around line 69-73: The current ErrorPage test only checks the document title
and does not validate the noIndex behavior controlled by SEOHead. Update the
NotFoundPage test to assert the robots meta tag (or equivalent SEOHead output)
includes noindex when NotFoundPage renders, using the existing
SEOHead/NotFoundPage symbols to locate the relevant setup. Keep the title
assertion only as a secondary check if needed, but make the main assertion
verify the noIndex prop’s effect.

In `@apps/web/src/pages/PrivacyPage.tsx`:
- Around line 32-72: The PrivacyPage is only partially localized because the
section body text is still hardcoded English while the headings already use
translation keys. Update each paragraph in PrivacyPage to use i18n keys
alongside the existing legal.privacy.s1.title through legal.privacy.s6.title
references, keeping the body copy in the locale files instead of inline
literals. Apply this consistently for all six sections so the rendered content
stays fully localized under tr and other locales.
- Line 27: The PrivacyPage date display is using the browser default formatting
instead of the app’s active language. Update PrivacyPage to destructure locale
from useTranslation() and pass that locale into the date formatting in the
legal.lastUpdated paragraph so the displayed date matches the translated page
language. Keep the change scoped to the existing PrivacyPage component and the
current new Date().toLocaleDateString() usage.

In `@openspec/changes/wave-3-frontend-structure/design.md`:
- Line 11: The design document has unlabeled fenced blocks that trigger
markdownlint; update the diagram fences in the markdown content to use an
explicit text fence instead of an untyped fence. Make the same change for both
affected fences in the document so the content stays identical while the docs
check passes.

In `@openspec/changes/wave-3-frontend-structure/proposal.md`:
- Line 11: The opening fenced diagram block in the proposal markdown is untyped
and will fail markdownlint; update the initial fence in the spec document to
include a language tag like text so the block is properly annotated. Locate the
opening fenced section in the proposal content and make the fence consistent
with the rest of the document’s markdown lint requirements.

In
`@openspec/changes/wave-3-frontend-structure/specs/web-shared-components/spec.md`:
- Around line 40-53: The BanDialog and useBanUser contracts are inconsistent:
BanDialog.onConfirm(reason) passes the typed reason, but useBanUser.confirmBan()
currently takes no arguments and relies on separate hook state. Update the spec
so a single flow is used across BanDialog, useBanUser, and AdminUserDetailPage:
either have the dialog set the hook’s reason state before confirmBan() runs, or
change confirmBan to accept the reason and thread it through consistently.
Ensure the chosen API is reflected in the BanDialogProps, useBanUser return
shape, and any consuming page/component requirements.

In `@openspec/changes/wave-3-frontend-structure/tasks.md`:
- Around line 12-17: The verification steps are in the wrong order, with test
running before check; update this task block so each `make check-*` step comes
before the corresponding `make test-*` step. Apply the same ordering
consistently in the later repeated verification steps, keeping the existing step
names like `make check-web`, `make test-web`, `make lint`, and `make test`
aligned in check-then-test sequence.
- Around line 265-273: Fix the malformed translation-key examples in the
AdminUsersPage task description: the instruction text currently contains broken
t(...) samples with missing closing punctuation, which can be misread or copied
incorrectly. Update the references around the AdminUsersPage literals so the
intended keys are written cleanly and consistently, especially the action/error
translation examples, while keeping the guidance aligned with the existing
t('admin.users.*') and t('common.*') symbols.
- Around line 3-7: The checklist text in tasks.md suggests bare folder imports
for shared components, but the repo requires explicit .ts/.tsx specifiers.
Update the HomePage import example to use only the explicit barrel path for
RecipeCard, and similarly remove any bare folder import guidance elsewhere in
the checklist (including the referenced form examples) so the instructions point
implementers to compliant import paths.

---

Outside diff comments:
In `@apps/web/src/pages/admin/AdminAuditLogPage.tsx`:
- Around line 60-65: The entity filter labels in AdminAuditLogPage are still
hardcoded English strings; update the select options in AdminAuditLogPage to use
t() for each user-visible label so the values for Users, Recipes, Equipment,
Vendors, Taste Notes, and Reports are translated consistently with the rest of
the admin page.

In `@apps/web/src/pages/admin/AdminCoffeeVarietiesPage.tsx`:
- Around line 590-592: The pagination label in AdminCoffeeVarietiesPage remains
hardcoded as “Page X of Y” while the surrounding controls are translated. Update
the span in the pagination footer to use the same i18n approach as the
previous/next buttons, replacing the literal text with a localized string that
interpolates page values. Refer to the pagination markup near the Page {page} of
{Math.ceil(total / 20)} display and keep the wording consistent with the
existing translation keys used in this component.

In `@apps/web/src/pages/admin/AdminCompatibilityPage.tsx`:
- Around line 75-83: The table header row in AdminCompatibilityPage still
hardcodes “Brew Method” and “Equipment Type” instead of using localized strings.
Update the header cells in the same table row as
t('admin.compatibility.compatible') to use translation keys from the existing
i18n setup, matching how Compatible is handled, so all three headers are
translated consistently.

In `@apps/web/src/pages/TermsPage.tsx`:
- Around line 29-73: The terms page is only partially localized because the
section body paragraphs in TermsPage still contain hardcoded English text while
the headings use t(). Update each section in TermsPage to read the paragraph
content from translation keys as well, using matching symbols like
legal.terms.s1.body through legal.terms.s6.body, and add those keys to the
locale files so both English and Turkish render fully translated content.

---

Nitpick comments:
In `@apps/web/src/components/admin/BanDialog.tsx`:
- Around line 28-35: The debug logging in BanDialog is inside the render path
and fires on every re-render, including each reason input change. Move the
log.debug call out of the component body and into a useEffect tied to the open
state in BanDialog so it only logs on open/close transitions, using the existing
BanDialogProps, user.id, and open values to keep the same context without
render-time spam.

In `@apps/web/src/hooks/useBanUser.test.ts`:
- Around line 1-161: The `useBanUser` tests still rely on `setReason` before
calling `confirmBan`, but `confirmBan` will now take the reason directly. Update
the `useBanUser` test cases that cover confirm-ban success, failure, and
empty-reason behavior to call `confirmBan('Spam')` or `confirmBan('')` on the
hook instance instead of setting state via `setReason`, and keep the assertions
around `banDialogUser`, `processing`, `error`, and `onSuccess` aligned with the
new `confirmBan` flow.

In `@apps/web/src/hooks/useBanUser.ts`:
- Line 84: The error handling in useBanUser duplicates the same message
extraction in both confirmBan and unban and still falls back to hardcoded
English text. Add a shared helper in useBanUser to derive the message from err,
then replace both inline expressions with that helper and pass translated
fallback strings from useTranslation() (for example via
t('admin.users.banFailed') and the corresponding unban key) so banError receives
localized messages consistently.

In `@apps/web/src/pages/admin/AdminCoffeeVarietiesPage.tsx`:
- Around line 515-525: The category badge in AdminCoffeeVarietiesPage is
rendering the raw enum value from item.category instead of the localized label
used elsewhere on the page. Update the table row badge rendering to map each
category value to the corresponding translation key (the same ones used by the
category filter/select, such as t('admin.coffeeVarieties.catVariety'),
t('admin.coffeeVarieties.catProcessing'), and
t('admin.coffeeVarieties.catMarketName')) before displaying it, while keeping
CATEGORY_BADGE_COLORS keyed by the underlying category value.

In `@apps/web/src/pages/admin/AdminUserEditPage.tsx`:
- Around line 244-251: The password label in AdminUserEditPage is reusing the
self-service reset key, which can give the wrong tone/context for an admin
editing another user. Update the label to use a dedicated admin-specific
translation key instead of t('auth.resetPassword.newPassword'), and add the
matching key under the admin users copy so the AdminUserEditPage form reads
naturally in this flow.

In `@apps/web/src/pages/admin/AdminUsersPage.test.tsx`:
- Around line 16-18: Add coverage for the ban/unban confirmation flow in
AdminUsersPage tests instead of stubbing BanDialog to a no-op. Update the
AdminUsersPage.test setup to render or simulate the BanDialog path so the
openBanDialog → confirm → adminApi.banUser and unban → adminApi.unbanUser
interactions are exercised. Use the existing AdminUsersPage, BanDialog, and
adminApi mocks to assert the typed reason is forwarded into banUser, and verify
the unban path still calls unbanUser correctly.

In `@openspec/changes/wave-3-frontend-structure/specs/error-pages/spec.md`:
- Line 213: The heading text for the error pages requirement is being misparsed
by markdownlint MD037 because the wildcard patterns in the title read like
emphasis. Update the heading in the spec so the `error.*` and `notFound.*`
tokens are treated as code or escaped consistently, keeping the requirement
wording intact while avoiding the lint warning.

In `@openspec/changes/wave-3-frontend-structure/specs/i18n/spec.md`:
- Around line 56-66: The i18n key-naming rules in the spec do not match the keys
that were actually delivered, so reconcile the documented convention with the
shipped translations. Update the spec in i18n/spec.md to reflect the real naming
used by the RecipeCreatePage, RecipeEditPage, RecipeComparePage, and related
entries (for example the createPage/editPage/form-style keys and
compare.notFound/compare.seoTitle), or adjust the delivered keys to match the
spec, but make them consistent. Use the existing section that lists D40 key
conventions and the referenced page-specific symbols to align the wording.

In `@packages/shared/src/i18n/i18n.test.ts`:
- Around line 14-24: The `every locale value is a string` test in `i18n.test.ts`
has redundant dead code because `expect(typeof value).toBe('string')` already
fails on non-strings. Remove the following `if (typeof value !== 'string') throw
...` checks from both the `enJson` and `trJson` loops, and keep the test using
the `Object.entries` iteration with the existing `expect` assertions only.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 21dc16fb-5fd8-47ad-94c0-cadd6850da1d

📥 Commits

Reviewing files that changed from the base of the PR and between cd16e5a and 13ce61b.

📒 Files selected for processing (67)
  • apps/web/src/components/ErrorBoundary.test.tsx
  • apps/web/src/components/ErrorBoundary.tsx
  • apps/web/src/components/admin/BanDialog.test.tsx
  • apps/web/src/components/admin/BanDialog.tsx
  • apps/web/src/components/form/Field.tsx
  • apps/web/src/components/form/Section.tsx
  • apps/web/src/components/form/index.ts
  • apps/web/src/hooks/useBanUser.test.ts
  • apps/web/src/hooks/useBanUser.ts
  • apps/web/src/pages/ErrorPage.test.tsx
  • apps/web/src/pages/ErrorPage.tsx
  • apps/web/src/pages/HomePage.tsx
  • apps/web/src/pages/NotFoundPage.tsx
  • apps/web/src/pages/PrivacyPage.test.tsx
  • apps/web/src/pages/PrivacyPage.tsx
  • apps/web/src/pages/TermsPage.test.tsx
  • apps/web/src/pages/TermsPage.tsx
  • apps/web/src/pages/admin/AdminAuditLogPage.test.tsx
  • apps/web/src/pages/admin/AdminAuditLogPage.tsx
  • apps/web/src/pages/admin/AdminBadgesPage.test.tsx
  • apps/web/src/pages/admin/AdminBadgesPage.tsx
  • apps/web/src/pages/admin/AdminCachePage.test.tsx
  • apps/web/src/pages/admin/AdminCachePage.tsx
  • apps/web/src/pages/admin/AdminCoffeeVarietiesPage.test.tsx
  • apps/web/src/pages/admin/AdminCoffeeVarietiesPage.tsx
  • apps/web/src/pages/admin/AdminCompatibilityPage.test.tsx
  • apps/web/src/pages/admin/AdminCompatibilityPage.tsx
  • apps/web/src/pages/admin/AdminDashboard.test.tsx
  • apps/web/src/pages/admin/AdminDashboard.tsx
  • apps/web/src/pages/admin/AdminEquipmentPage.test.tsx
  • apps/web/src/pages/admin/AdminEquipmentPage.tsx
  • apps/web/src/pages/admin/AdminLayout.test.tsx
  • apps/web/src/pages/admin/AdminLayout.tsx
  • apps/web/src/pages/admin/AdminRecipesPage.test.tsx
  • apps/web/src/pages/admin/AdminRecipesPage.tsx
  • apps/web/src/pages/admin/AdminTasteNotesPage.test.tsx
  • apps/web/src/pages/admin/AdminTasteNotesPage.tsx
  • apps/web/src/pages/admin/AdminUserCreatePage.test.tsx
  • apps/web/src/pages/admin/AdminUserCreatePage.tsx
  • apps/web/src/pages/admin/AdminUserDetailPage.test.tsx
  • apps/web/src/pages/admin/AdminUserDetailPage.tsx
  • apps/web/src/pages/admin/AdminUserEditPage.test.tsx
  • apps/web/src/pages/admin/AdminUserEditPage.tsx
  • apps/web/src/pages/admin/AdminUsersPage.test.tsx
  • apps/web/src/pages/admin/AdminUsersPage.tsx
  • apps/web/src/pages/admin/AdminVendorsPage.test.tsx
  • apps/web/src/pages/admin/AdminVendorsPage.tsx
  • apps/web/src/pages/auth/VerifyEmailPage.test.tsx
  • apps/web/src/pages/auth/VerifyEmailPage.tsx
  • apps/web/src/pages/recipes/RecipeComparePage.test.tsx
  • apps/web/src/pages/recipes/RecipeComparePage.tsx
  • apps/web/src/pages/recipes/RecipeCreatePage.tsx
  • apps/web/src/pages/recipes/RecipeEditPage.test.tsx
  • apps/web/src/pages/recipes/RecipeEditPage.tsx
  • apps/web/src/router.tsx
  • openspec/changes/wave-3-frontend-structure/design.md
  • openspec/changes/wave-3-frontend-structure/proposal.md
  • openspec/changes/wave-3-frontend-structure/specs/error-pages/spec.md
  • openspec/changes/wave-3-frontend-structure/specs/i18n/spec.md
  • openspec/changes/wave-3-frontend-structure/specs/recipe-list/spec.md
  • openspec/changes/wave-3-frontend-structure/specs/web-shared-components/spec.md
  • openspec/changes/wave-3-frontend-structure/tasks.md
  • packages/shared/src/i18n/en.json
  • packages/shared/src/i18n/i18n.test.ts
  • packages/shared/src/i18n/tr.json
  • plans/ROADMAP.md
  • plans/TECHNICAL_DEBT.md
💤 Files with no reviewable changes (1)
  • apps/web/src/pages/NotFoundPage.tsx

Comment thread apps/web/src/pages/admin/AdminCachePage.tsx
Comment thread apps/web/src/pages/admin/AdminCachePage.tsx
Comment on lines +24 to +34
const {
banDialogUser,
processing: banProcessing,
error: banError,
openBanDialog,
confirmBan,
unban,
closeDialog,
} = useBanUser((_userId, isBanned) => {
setUser((prev) => prev ? { ...prev, isBanned } : prev);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Downstream impact of the useBanUser/BanDialog contract mismatch.

confirmBan is passed directly as onConfirm={confirmBan} (Line 310) without ever calling the hook's reason/setReason (not even destructured here). Root cause and proposed fix are in useBanUser.ts; once confirmBan is updated to accept reason as a parameter, this wiring will work correctly as-is (no change needed here beyond the hook's own fix).

Also applies to: 305-313

🤖 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/web/src/pages/admin/AdminUserDetailPage.tsx` around lines 24 - 34,
Update the useBanUser/BanDialog contract so confirmBan accepts the ban reason
directly instead of relying on internal state that AdminUserDetailPage does not
pass through. Fix the hook in useBanUser.ts by threading reason into confirmBan
and using it when submitting the ban, and keep AdminUserDetailPage’s onConfirm
wiring as-is since it already passes confirmBan directly. Ensure the hook’s
returned API matches the BanDialog expectations, including any reason-related
state or setters that are no longer needed.

Comment thread apps/web/src/pages/auth/VerifyEmailPage.tsx
Comment thread apps/web/src/pages/ErrorPage.test.tsx
them into one change keeps the sequencing coherent, lets the i18n work (D40) land its key sets
correctly the first time, and produces a single spec archive that documents the frontend structural
shift as one unit.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Tag the opening diagram fence.

This untyped fenced block will trip markdownlint. Add a language tag such as text to keep the spec doc lint-clean.

Suggested fix
-```
+```text
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
🤖 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 `@openspec/changes/wave-3-frontend-structure/proposal.md` at line 11, The
opening fenced diagram block in the proposal markdown is untyped and will fail
markdownlint; update the initial fence in the spec document to include a
language tag like text so the block is properly annotated. Locate the opening
fenced section in the proposal content and make the fence consistent with the
rest of the document’s markdown lint requirements.

Comment thread openspec/changes/wave-3-frontend-structure/tasks.md
Comment thread openspec/changes/wave-3-frontend-structure/tasks.md Outdated
Comment thread openspec/changes/wave-3-frontend-structure/tasks.md
- Field.tsx: wrap children in <label> for accessible label/input pairing
- AdminCachePage.tsx: i18n body text + status flag for message styling
- useBanUser.ts: confirmBan accepts reason directly; shared getErrorMessage helper
- BanDialog.tsx: move log.debug from render path into useEffect on open
- useBanUser.test.ts: align tests with new confirmBan(reason) API
- AdminUsersPage.test.tsx: real BanDialog coverage for ban/unban flow
- AdminUserDetailPage.tsx: compatible with confirmBan(reason) (noop)
- VerifyEmailPage.tsx: always use t('verifyEmail.failed'), not err.message
- PrivacyPage/TermsPage: i18n body text + locale-aware date formatting
- AdminAuditLogPage: entity filter labels via t()
- AdminCoffeeVarietiesPage: pagination via common.pagination key; translated
  category badge
- AdminCompatibilityPage: table headers via t()
- AdminUserEditPage: admin-specific key for password label
- ErrorPage.test.tsx: verify robots noindex meta tag
- i18n.test.ts: remove redundant dead-code throw statements
- en.json/tr.json: add 30+ new translation keys
- openspec docs: untyped fences, task ordering, malformed t(), bare imports,
  spec consistency for confirmBan API and i18n key naming

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
apps/web/src/hooks/useBanUser.ts (1)

78-92: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

unban lacks an in-flight guard. The unban buttons in apps/web/src/pages/admin/AdminUsersPage.tsx and apps/web/src/pages/admin/AdminUserDetailPage.tsx call unban() directly and never disable on processing, so a fast double-click can send duplicate unbanUser requests. Wire the unban path to processing or add a local guard before the request.

🤖 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/web/src/hooks/useBanUser.ts` around lines 78 - 92, The unban path in
useBanUser lacks an in-flight guard, so repeated clicks can trigger duplicate
adminApi.unbanUser requests. Add protection in the unban callback itself by
checking processing or a local pending flag before starting the request, and
make sure the AdminUsersPage/AdminUserDetailPage unban actions use that guarded
unban flow so a second click is ignored while the first request is running.
🧹 Nitpick comments (1)
apps/web/src/hooks/useBanUser.ts (1)

12-19: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Repeated inline user-shape type.

The { id: string; username: string; displayName: string | null } shape is duplicated across banDialogUser, openBanDialog's parameter, and the internal state declaration. Extracting a BanDialogUser type would reduce duplication.

🤖 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/web/src/hooks/useBanUser.ts` around lines 12 - 19, The user object shape
is duplicated in useBanUser across banDialogUser, openBanDialog, and the related
state, so extract it into a shared BanDialogUser type. Update the useBanUser
hook’s type declarations to reference that alias everywhere the same { id,
username, displayName } shape is used, keeping the function signature and state
consistent without repeating the inline object type.
🤖 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/web/src/components/form/Field.tsx`:
- Around line 14-24: The Field component is applying typography classes on the
wrapping label, which causes nested form controls to inherit `text-sm
font-medium` under Tailwind v4 Preflight. Update Field so the typography classes
are scoped to the inner label text span (or explicitly reset them on the child
control), and keep the wrapper label only for layout/color; use the Field and
FieldProps symbols to locate the JSX in the Field component.

In `@apps/web/src/hooks/useBanUser.ts`:
- Around line 22-24: getErrorMessage in useBanUser is leaking raw backend
err.message and using hardcoded English fallbacks, so update the hook to stop
returning server text entirely and instead return a stable error identifier or
non-null fallback that can be translated by the caller. Adjust the ban/unban
handling in useBanUser so the consuming page translates the result with t(),
matching the localized pattern already used for admin.users.removeAdminError and
the VerifyEmailPage fix. Keep the hook focused on mapping errors, and move any
user-facing message selection out of getErrorMessage.

---

Outside diff comments:
In `@apps/web/src/hooks/useBanUser.ts`:
- Around line 78-92: The unban path in useBanUser lacks an in-flight guard, so
repeated clicks can trigger duplicate adminApi.unbanUser requests. Add
protection in the unban callback itself by checking processing or a local
pending flag before starting the request, and make sure the
AdminUsersPage/AdminUserDetailPage unban actions use that guarded unban flow so
a second click is ignored while the first request is running.

---

Nitpick comments:
In `@apps/web/src/hooks/useBanUser.ts`:
- Around line 12-19: The user object shape is duplicated in useBanUser across
banDialogUser, openBanDialog, and the related state, so extract it into a shared
BanDialogUser type. Update the useBanUser hook’s type declarations to reference
that alias everywhere the same { id, username, displayName } shape is used,
keeping the function signature and state consistent without repeating the inline
object type.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2c3521e6-cc3e-4608-9be1-75637a4c6dde

📥 Commits

Reviewing files that changed from the base of the PR and between 13ce61b and ea3a899.

📒 Files selected for processing (22)
  • apps/web/src/components/admin/BanDialog.tsx
  • apps/web/src/components/form/Field.tsx
  • apps/web/src/hooks/useBanUser.test.ts
  • apps/web/src/hooks/useBanUser.ts
  • apps/web/src/pages/ErrorPage.test.tsx
  • apps/web/src/pages/PrivacyPage.tsx
  • apps/web/src/pages/TermsPage.tsx
  • apps/web/src/pages/admin/AdminAuditLogPage.tsx
  • apps/web/src/pages/admin/AdminCachePage.tsx
  • apps/web/src/pages/admin/AdminCoffeeVarietiesPage.tsx
  • apps/web/src/pages/admin/AdminCompatibilityPage.tsx
  • apps/web/src/pages/admin/AdminUserEditPage.tsx
  • apps/web/src/pages/admin/AdminUsersPage.test.tsx
  • apps/web/src/pages/auth/VerifyEmailPage.tsx
  • openspec/changes/wave-3-frontend-structure/design.md
  • openspec/changes/wave-3-frontend-structure/specs/error-pages/spec.md
  • openspec/changes/wave-3-frontend-structure/specs/i18n/spec.md
  • openspec/changes/wave-3-frontend-structure/specs/web-shared-components/spec.md
  • openspec/changes/wave-3-frontend-structure/tasks.md
  • packages/shared/src/i18n/en.json
  • packages/shared/src/i18n/i18n.test.ts
  • packages/shared/src/i18n/tr.json
✅ Files skipped from review due to trivial changes (5)
  • apps/web/src/pages/admin/AdminAuditLogPage.tsx
  • openspec/changes/wave-3-frontend-structure/tasks.md
  • apps/web/src/pages/admin/AdminUserEditPage.tsx
  • openspec/changes/wave-3-frontend-structure/design.md
  • packages/shared/src/i18n/tr.json
🚧 Files skipped from review as they are similar to previous changes (12)
  • apps/web/src/components/admin/BanDialog.tsx
  • packages/shared/src/i18n/i18n.test.ts
  • apps/web/src/pages/admin/AdminCompatibilityPage.tsx
  • apps/web/src/pages/admin/AdminCachePage.tsx
  • packages/shared/src/i18n/en.json
  • apps/web/src/pages/TermsPage.tsx
  • apps/web/src/pages/PrivacyPage.tsx
  • apps/web/src/pages/ErrorPage.test.tsx
  • openspec/changes/wave-3-frontend-structure/specs/web-shared-components/spec.md
  • apps/web/src/pages/admin/AdminCoffeeVarietiesPage.tsx
  • openspec/changes/wave-3-frontend-structure/specs/error-pages/spec.md
  • openspec/changes/wave-3-frontend-structure/specs/i18n/spec.md

Comment thread apps/web/src/components/form/Field.tsx
Comment thread apps/web/src/hooks/useBanUser.ts Outdated
@Ardakilic Ardakilic merged commit 76f6d08 into main Jul 6, 2026
7 checks passed
@Ardakilic Ardakilic deleted the feat/wave3 branch July 6, 2026 19:39
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.

1 participant