Skip to content

(SP: 2) [App] Upgrade Next.js dependencies and format codebase#355

Merged
ViktorSvertoka merged 1 commit intodevelopfrom
fix/refactoring
Feb 22, 2026
Merged

(SP: 2) [App] Upgrade Next.js dependencies and format codebase#355
ViktorSvertoka merged 1 commit intodevelopfrom
fix/refactoring

Conversation

@ViktorSvertoka
Copy link
Member

@ViktorSvertoka ViktorSvertoka commented Feb 22, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added tooltip display for heatmap daily activity data with improved interaction handling.
    • Enhanced admin quiz management to support translation updates across multiple locales during patch operations.
  • Improvements

    • Reorganized admin order and product page layouts for improved usability and information hierarchy.
    • Added validation to ensure comprehensive locale coverage for quiz translations during publication.
  • Dependencies

    • Updated Next.js to version 16.1.6.
  • Refactor

    • Extensive code formatting and structural improvements across components and utilities.

@ViktorSvertoka ViktorSvertoka self-assigned this Feb 22, 2026
@ViktorSvertoka ViktorSvertoka added bug Something isn't working refactor Code restructuring without functional changes setup Configuration, tooling, infrastructure setup labels Feb 22, 2026
@vercel
Copy link
Contributor

vercel bot commented Feb 22, 2026

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

Project Deployment Actions Updated (UTC)
devlovers-net Ready Ready Preview, Comment Feb 22, 2026 3:45pm

@netlify
Copy link

netlify bot commented Feb 22, 2026

Deploy Preview for develop-devlovers ready!

Name Link
🔨 Latest commit c1d7c3c
🔍 Latest deploy log https://app.netlify.com/projects/develop-devlovers/deploys/699b246cd071da00085b58a7
😎 Deploy Preview https://deploy-preview-355--develop-devlovers.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 22, 2026

📝 Walkthrough

Walkthrough

This is a large-scale code styling and formatting overhaul across the entire frontend codebase, including import statement restructuring, JSX indentation adjustments, Tailwind CSS class reordering, JSON snapshot formatting, and quote style standardization. Minimal logic changes; primarily cosmetic refactoring to improve consistency and readability.

Changes

Cohort / File(s) Summary
Admin Shop Pages
frontend/app/[locale]/admin/shop/orders/[id]/page.tsx, frontend/app/[locale]/admin/shop/orders/page.tsx, frontend/app/[locale]/admin/shop/products/page.tsx
Significant layout restructuring: orders page now features prominent header with Back/Refund controls, distinct summary/stock sections, and consolidated items table; products page expanded to render unified mobile/desktop views with consistent badge handling and per-row actions; both refactor UI flow and field ordering while preserving data logic.
Admin Quiz API Routes
frontend/app/api/admin/quiz/[id]/route.ts, frontend/app/api/admin/quiz/[id]/questions/route.ts, frontend/app/api/admin/quiz/[id]/questions/[questionId]/route.ts, frontend/app/api/admin/quiz/route.ts, frontend/app/api/admin/categories/route.ts
JSON error response formatting adjusted to multi-line object literals; PATCH flow in quiz route adds translation update logic for multiple locales (en, uk, pl) via onConflictDoUpdate; catch blocks expanded to handle AdminUnauthorizedError; all changes preserve status codes and error semantics.
Dashboard Components
frontend/components/dashboard/ActivityHeatmapCard.tsx, frontend/components/dashboard/AchievementBadge.tsx, frontend/components/dashboard/StatsCard.tsx, frontend/components/dashboard/ProfileCard.tsx, frontend/components/dashboard/QuizResultRow.tsx, frontend/components/dashboard/ExplainedTermsCard.tsx, frontend/components/dashboard/FeedbackForm.tsx, frontend/components/dashboard/AchievementsSection.tsx
Widespread formatting, spacing, and class reordering with minor layout adjustments; ActivityHeatmapCard adds tooltip state and click-outside handler for improved interactivity; ProfileCard refactors flex/grid wrappers and border styling; remaining components preserve logic while adjusting presentation.
Quiz & Admin Components
frontend/components/admin/quiz/QuestionEditor.tsx, frontend/components/admin/quiz/QuizListTable.tsx, frontend/components/admin/quiz/QuizMetadataEditor.tsx, frontend/components/admin/quiz/ExplanationEditor.tsx, frontend/components/admin/quiz/LocaleTabs.tsx, frontend/components/admin/quiz/AnswerEditor.tsx, frontend/components/quiz/QuizContainer.tsx, frontend/components/quiz/QuizCard.tsx
Minor CSS class updates (focus:outline-none additions, class reordering); QuizContainer COMPLETE_QUIZ payload restructured to multi-line formatting; LocaleTabs adjusts badge position offsets; AnswerEditor adds focus outline removal; all changes preserve functional logic.
Home/Landing Components
frontend/components/home/FeaturesHeroSection.tsx, frontend/components/home/InteractiveCTAButton.tsx, frontend/components/home/FloatingCode.tsx, frontend/components/home/WelcomeHeroSection.tsx, frontend/components/home/FlipCardQA.tsx, frontend/components/home/InteractiveConstellation.tsx
JSX attribute formatting and className reordering; InteractiveCTAButton extends textVariants array and adjusts rotation behavior structure; FloatingCode refactors line-number rendering and style propagation; WelcomeHeroSection adjusts transition definitions and class list; all preserve behavioral semantics.
Header & Navigation Components
frontend/components/header/MainSwitcher.tsx, frontend/components/header/AppMobileMenu.tsx, frontend/components/header/MobileMenuContext.tsx, frontend/components/admin/AdminSidebar.tsx, frontend/components/about/PricingSection.tsx
Minor formatting and class reordering; MainSwitcher refactors conditional formatting across multiple lines; AppMobileMenu reformats listener options and className structure; AdminSidebar adjusts nav item compression and class ordering; all preserve navigation logic.
Leaderboard Components
frontend/components/leaderboard/LeaderboardPodium.tsx, frontend/components/leaderboard/LeaderboardTable.tsx, frontend/components/leaderboard/AchievementPips.tsx, frontend/components/leaderboard/types.ts
Minimal changes: string and JSX attribute reflow; LeaderboardTable reorders cellClass and adjusts padding utilities; AchievementPips reformats useSyncExternalStore hook; EOL newline additions; behavior preserved.
Q&A Components
frontend/components/q&a/AccordionList.tsx, frontend/components/q&a/AIWordHelper.tsx, frontend/components/q&a/HighlightCachedTerms.tsx, frontend/components/q&a/Pagination.tsx, frontend/components/q&a/QaSection.tsx
Tailwind class reordering (cursor-pointer/select-none/inline-block order swaps, animation-related class repositioning); all maintain visual and functional equivalence.
Page Components
frontend/app/[locale]/achievements-demo/page.tsx, frontend/app/[locale]/admin/quiz/[id]/page.tsx, frontend/app/[locale]/admin/shop/page.tsx, frontend/app/[locale]/admin/shop/products/[id]/edit/page.tsx, frontend/app/[locale]/admin/shop/products/new/page.tsx, frontend/app/[locale]/dashboard/page.tsx, frontend/app/[locale]/dashboard/quiz-review/[attemptId]/page.tsx, frontend/app/[locale]/leaderboard/page.tsx, frontend/app/[locale]/quiz/[slug]/page.tsx, frontend/app/[locale]/quizzes/page.tsx, frontend/app/[locale]/shop/products/[slug]/page.tsx
Import statement reformatting (multi-line vs single-line); JSX indentation and class adjustments; comment spacing tweaks; all preserve functional logic and control flow.
API & Auth Routes
frontend/app/api/auth/password-reset/confirm/route.ts, frontend/app/api/auth/signup/route.ts, frontend/app/api/feedback/route.ts, frontend/app/api/quiz/verify-answer/route.ts, frontend/app/api/shop/orders/[id]/status/route.ts, frontend/app/global-error.tsx
Trailing newline additions; import quote style changes (double to single quotes); JSON error response reformatting; all preserve error handling and status codes.
Validation & Schema Files
frontend/lib/validation/admin-quiz.ts, frontend/lib/validation/quiz-publish-validation.ts
patchQuestionSchema and patchQuizSchema restructured to fluent chaining (z.object(...).refine(...)) instead of inline refine; new locale validation in validateQuizForPublish ensures coverage across all LOCALES; preserves field names and error messages.
Achievements & Utilities
frontend/lib/achievements.ts, frontend/lib/admin/tiptap-transforms.ts, frontend/lib/auth/password-bytes.ts, frontend/lib/auth/signup-constraints.ts, frontend/lib/cart.ts, frontend/lib/env/monobank.ts, frontend/lib/github-stars.ts, frontend/lib/psp/monobank.ts, frontend/lib/quiz/quiz-answers-redis.ts, frontend/lib/shop/status-token.ts
ACHIEVEMENTS array expanded to multi-line object literals; minor function signature reflowing and whitespace adjustments across imports and logic blocks; no behavioral changes to core computations.
Order Services & Database
frontend/lib/services/orders/checkout.ts, frontend/lib/services/orders/monobank-cancel-payment.ts, frontend/lib/services/orders/monobank-janitor.ts, frontend/lib/services/orders/summary.ts, frontend/lib/services/orders/monobank/merchant-paym-info.ts, frontend/db/queries/categories/admin-categories.ts, frontend/db/queries/quizzes/admin-quiz.ts, frontend/db/queries/quizzes/quiz.ts, frontend/db/queries/users.ts, frontend/db/seed-questions.ts
Formatting/indentation adjustments in function signatures and multi-line calls; empty catch block collapsed to single line; getMaxQuizDisplayOrder signature reflowed to multi-line; on-conflict targeting reformatted; all preserve query logic and return types.
Test Files
frontend/lib/tests/shop/*, frontend/lib/tests/q&a/questions-route.test.ts, frontend/instrumentation.ts
Import reformatting, quote style changes (double to single), test assertion reflow, mock object literal adjustments; monobank-cancel-payment-route-f5.test.ts introduces try/finally guards and separates response validation from side-effect validation; no changes to test assertions or expected behaviors.
UI Component Primitives & Global Styles
frontend/components/ui/accordion.tsx, frontend/components/ui/badge.tsx, frontend/components/ui/particle-canvas.tsx, frontend/app/globals.css, frontend/components/dashboard/QuizResultsSection.tsx, frontend/components/dashboard/QuizReviewCard.tsx, frontend/components/dashboard/QuizReviewList.tsx, frontend/components/dashboard/QuizSavedBanner.tsx, frontend/components/quiz/QuizQuestion.tsx, frontend/components/quiz/QuizResult.tsx, frontend/components/quiz/ViolationsCounter.tsx, frontend/components/auth/*
CSS polygon/transform/color-mix formatting reflowed across lines; Tailwind class reordering; BadgeProps.variant union reformatted to multi-line; trailing newlines added; no changes to visual output or component behavior.
Configuration & Documentation
frontend/next.config.ts, frontend/package.json, frontend/docs/*, frontend/drizzle/meta/*.json, frontend/actions/quiz.ts
Sentry config quote changes (double to single); next dependency updated ^16.0.7 → ^16.1.6; seed:users script removed; JSON snapshots consolidated from multi-line arrays to single-line arrays; resolveRequestIdentifier import reformatted; markdown documentation reflowed for readability.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • AM1007

🐰 Whiskers twitch with delight,
A thousand files shine just right,
Classes reordered, imports refined,
Every comma in its place, we find,
Beauty in the formatting light!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the two main changes: upgrading Next.js dependencies and formatting the codebase, matching the primary objectives reflected in the raw summary.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/refactoring

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (2)
frontend/lib/psp/monobank.ts (1)

874-893: ⚠️ Potential issue | 🔴 Critical

Concurrent force-refresh stampede risk on key rotation — verified.

If multiple webhook requests arrive simultaneously and all fail initial signature verification (e.g., after a key rotation), each independently enters the force-refresh block and calls fetchWebhookPubKey({ forceRefresh: true }). The module-level cache is bypassed on force-refresh, and there is no in-flight deduplication, so N concurrent failing requests generate N parallel network calls to the Monobank pubkey endpoint. This can trigger rate limiting or amplify cascading failures.

Implement module-level in-flight promise deduplication to collapse concurrent force-refresh calls:

🔒 Proposed fix — deduplicate concurrent force-refresh calls
+let _pubkeyRefreshInFlight: Promise<Uint8Array> | null = null;
+
+async function fetchWebhookPubKeyWithDedup(): Promise<Uint8Array> {
+  if (_pubkeyRefreshInFlight) return _pubkeyRefreshInFlight;
+  _pubkeyRefreshInFlight = fetchWebhookPubKey({ forceRefresh: true }).finally(
+    () => { _pubkeyRefreshInFlight = null; }
+  );
+  return _pubkeyRefreshInFlight;
+}

   try {
-    const refreshed = await fetchWebhookPubKey({ forceRefresh: true });
+    const refreshed = await fetchWebhookPubKeyWithDedup();
     const ok = verifyWebhookSignature(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/lib/psp/monobank.ts` around lines 874 - 893, The issue: concurrent
webhook requests that fail verification each call fetchWebhookPubKey({
forceRefresh: true }) and cause a stampede; fix by adding module-level in-flight
deduplication for force-refresh calls so only one network fetch runs at a time.
Implement a shared variable (e.g., inFlightFetchWebhookPubKeyPromise) in the
same module and update fetchWebhookPubKey to return the existing promise when a
forceRefresh request is already pending; ensure the promise is cleared on both
success and error so future refreshes retry, and keep existing behavior for
non-forceRefresh calls and for verifyWebhookSignature/monoLogInfo/monoLogError
use unchanged.
frontend/db/queries/users.ts (1)

48-65: ⚠️ Potential issue | 🟡 Minor

Raw SQL bypasses Drizzle schema; type assertion with any[] discards type safety.

Two issues visible in this function:

  1. Hardcoded names in raw SQLpoint_transactions, user_id, and points are literal strings, not derived from the imported pointTransactions schema object. A table or column rename via migration won't be caught at compile time. Compare to getUserProfile, which uses ${pointTransactions.points} and pointTransactions.userId to bind schema references.

  2. Unsafe type assertionresult as { rows: any[] } loses type information. The .rows property is valid for the PostgreSQL adapters in use (node-postgres and neon-http), but using any[] discards row typing. Replace with a type guard or properly typed assertion (e.g., as { rows: Array<{ rank: number }> }) to preserve type safety.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/db/queries/users.ts` around lines 48 - 65, Replace the raw SQL text
in rankQuery with schema-bound identifiers from the imported pointTransactions
object (use `${pointTransactions}`, `${pointTransactions.userId}`,
`${pointTransactions.points}` or their appropriate column symbols) so
table/column names are tied to the Drizzle schema, and update the db.execute
result typing by replacing `result as { rows: any[] }` with a properly typed
assertion or type guard (e.g., `as { rows: Array<{ rank: number }> }`) so
rankRow preserves its shape; reference the existing symbols rankQuery,
pointTransactions, db.execute, and rankRow when making these changes.
🧹 Nitpick comments (31)
frontend/lib/auth/password-bytes.ts (1)

1-3: Consider hoisting TextEncoder to module scope.

A new TextEncoder instance is allocated on every call. Moving it to a module-level constant is a zero-risk micro-optimisation that avoids repeated allocations, especially relevant if this is called during form validation on every keystroke.

♻️ Proposed refactor
+const encoder = new TextEncoder();
+
 export function utf8ByteLength(value: string): number {
-  return new TextEncoder().encode(value).length;
+  return encoder.encode(value).length;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/lib/auth/password-bytes.ts` around lines 1 - 3, Hoist the
TextEncoder instance to module scope and reuse it inside utf8ByteLength to avoid
allocating a new encoder on every call; create a module-level constant (e.g.,
const textEncoder = new TextEncoder()) and change utf8ByteLength(value: string)
to call textEncoder.encode(value).length so the function uses the shared encoder
instead of instantiating one each invocation.
frontend/components/header/MobileMenuContext.tsx (1)

76-76: Hardcoded locale list in regex may silently break the same-page guard.

pathname.replace(/^\/(en|uk|pl)/, '') hard-codes supported locales. If a new locale is added to the i18n config without updating this regex, strippedPathname retains the prefix and the equality check on line 78 always fails, causing a redundant router.push for same-page links instead of just closing the menu.

Consider deriving the supported locales from a single source of truth (e.g., the i18n config) or using a generic locale-segment regex:

♻️ Suggested approach
-const strippedPathname = pathname.replace(/^\/(en|uk|pl)/, '') || '/';
+// Import `locales` array from your i18n config so this stays in sync automatically
+import { locales } from '@/i18n/config';
+const localePattern = new RegExp(`^\\/(${locales.join('|')})`);
+const strippedPathname = pathname.replace(localePattern, '') || '/';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/header/MobileMenuContext.tsx` at line 76, The runtime bug
is caused by hardcoding the locale list in the regex used to compute
strippedPathname (const strippedPathname = pathname.replace(/^\/(en|uk|pl)/, '')
|| '/'), which will keep a new locale prefix and break the same-page equality
check; update the logic in MobileMenuContext to derive supported locales from
the app's i18n config (or use a generic locale-segment regex like
/^\/[a-z]{2}(-[A-Z]{2})?/) and then strip that dynamic prefix from pathname when
computing strippedPathname so the equality check that prevents router.push works
for all configured locales (modify the code around strippedPathname and where
same-page navigation is checked/handled).
frontend/components/auth/ResetPasswordForm.tsx (1)

21-23: Remove the local utf8ByteLength — it duplicates the shared lib export.

frontend/lib/auth/password-bytes.ts already exports an identical utf8ByteLength. The local redefinition diverges silently if the canonical implementation ever changes.

♻️ Proposed fix

Remove the local definition and add an import:

 import { Button } from '@/components/ui/button';
 import {
   PASSWORD_MAX_BYTES,
   PASSWORD_MIN_LEN,
   PASSWORD_POLICY_REGEX,
 } from '@/lib/auth/signup-constraints';
+import { utf8ByteLength } from '@/lib/auth/password-bytes';

 type ResetPasswordFormProps = {
   token: string;
 };

-function utf8ByteLength(value: string): number {
-  return new TextEncoder().encode(value).length;
-}
-
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/auth/ResetPasswordForm.tsx` around lines 21 - 23, Remove
the local utf8ByteLength function in ResetPasswordForm.tsx and import the shared
implementation instead; replace the local definition with an import of
utf8ByteLength from the shared password-bytes module and ensure all uses in
ResetPasswordForm reference the imported utf8ByteLength symbol.
frontend/lib/shop/status-token.ts (1)

106-107: Braceless if is inconsistent with the braced style used immediately below.

Line 108 uses a braced block for a similar guard. Keeping the style consistent reduces future "dangling else" / accidental multi-statement risks.

♻️ Suggested consistency fix
-  if (!payload || payload.v !== 1)
-    return { ok: false, reason: 'invalid_payload' };
+  if (!payload || payload.v !== 1) {
+    return { ok: false, reason: 'invalid_payload' };
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/lib/shop/status-token.ts` around lines 106 - 107, The single-line
guard "if (!payload || payload.v !== 1) return { ok: false, reason:
'invalid_payload' };" should be converted to a braced block to match the braced
style used immediately below: replace the one-line if with a block form (e.g.,
if (!payload || payload.v !== 1) { return { ok: false, reason: 'invalid_payload'
}; }) so the guard around the payload/v check uses braces and reduces accidental
multi-statement errors; locate the check around the payload.v verification in
status-token.ts and update that if statement accordingly.
frontend/lib/admin/tiptap-transforms.ts (1)

32-35: Consider adding braces to the multi-line if bodies for consistency and safety.

Lines 32–35 drop curly braces while using a two-line body, yet the immediately adjacent line 36 uses an inline one-liner, creating a mixed style within the same block. Brace-less multi-line if bodies are the most common source of "off-by-indentation" bugs when either branch is later extended.

♻️ Suggested fix
-        if (child.type === 'bulletList')
-          return listBlockToTipTap(child, 'bulletList');
-        if (child.type === 'numberedList')
-          return listBlockToTipTap(child, 'orderedList');
-        if (child.type === 'code') return codeBlockToTipTap(child);
+        if (child.type === 'bulletList') return listBlockToTipTap(child, 'bulletList');
+        if (child.type === 'numberedList') return listBlockToTipTap(child, 'orderedList');
+        if (child.type === 'code') return codeBlockToTipTap(child);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/lib/admin/tiptap-transforms.ts` around lines 32 - 35, The two
multi-line if branches checking child.type ('bulletList' and 'numberedList')
call listBlockToTipTap but omit braces; add curly braces around each of those if
bodies so they match the surrounding style and avoid future indentation
bugs—specifically update the branches that invoke listBlockToTipTap(child,
'bulletList') and listBlockToTipTap(child, 'orderedList') to use { ... } blocks
within the same function that processes child nodes.
frontend/drizzle/meta/0006_snapshot.json (1)

1-2694: Consider excluding machine-generated Drizzle snapshots from the formatter.

All changes here are correctly formatted and semantically identical to the previous version — no data, FK mappings, or constraints were altered. However, Drizzle Kit regenerates drizzle/meta/*.json on every drizzle-kit generate or drizzle-kit push, using its own formatting style. Running Prettier over these files will cause recurring diff churn each time the migration toolchain runs. The standard practice is to add frontend/drizzle/meta/ (and similar generated paths like drizzle/migrations/) to .prettierignore.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/drizzle/meta/0006_snapshot.json` around lines 1 - 2694, The
Drizzle-generated snapshot JSONs (e.g., the file with id
"a2bbe8f7-7211-4508-ac7b-5a1f495d31cc" under frontend/drizzle/meta/) are
machine-regenerated and cause pointless Prettier churn; update the project's
.prettierignore to exclude the generated Drizzle metadata and migrations by
adding entries for frontend/drizzle/meta/ and the Drizzle migrations folder
(e.g., drizzle/migrations/) so files like
frontend/drizzle/meta/0006_snapshot.json are not formatted by Prettier.
frontend/lib/env/monobank.ts (2)

21-21: MonobankConfig type is declared after it is used.

getMonobankConfig references MonobankConfig at line 21 while the type declaration appears at line 41. TypeScript hoists type declarations so this compiles cleanly, but the conventional style is to declare types before their first use. Consider moving the MonobankConfig type above getMonobankConfig.

Also applies to: 41-51

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/lib/env/monobank.ts` at line 21, Move the MonobankConfig type
declaration so it appears before its first use in getMonobankConfig;
specifically, place the MonobankConfig type (and any other related types
currently declared in the 41-51 region) above the getMonobankConfig function so
the type definitions precede references to them.

59-69: parseTimeoutMs and parsePositiveInt are functionally identical — deduplicate.

Both functions have the exact same implementation. parseTimeoutMs should either be removed and replaced with parsePositiveInt, or both should be unified under a single name that's semantically neutral (e.g., parsePosInt).

♻️ Proposed refactor
-function parseTimeoutMs(raw: string | undefined, fallback: number): number {
-  const v = raw ? Number.parseInt(raw, 10) : NaN;
-  if (!Number.isFinite(v) || v <= 0) return fallback;
-  return v;
-}
-
 function parsePositiveInt(raw: string | undefined, fallback: number): number {
   const v = raw ? Number.parseInt(raw, 10) : NaN;
   if (!Number.isFinite(v) || v <= 0) return fallback;
   return v;
 }

Then update the call site in getMonobankEnv:

-  const invoiceTimeoutMs = parseTimeoutMs(
+  const invoiceTimeoutMs = parsePositiveInt(
     env.MONO_INVOICE_TIMEOUT_MS,
     runtimeEnv.NODE_ENV === 'production' ? 8000 : 12000
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/lib/env/monobank.ts` around lines 59 - 69, The two identical
functions parseTimeoutMs and parsePositiveInt should be deduplicated: remove one
(e.g., delete parseTimeoutMs) and keep a single semantically neutral function
(e.g., parsePositiveInt or parsePosInt) with the same implementation, then
update all call sites (notably getMonobankEnv) to call the retained function
name; ensure exports/usage are updated accordingly so no references to the
removed name remain.
frontend/lib/psp/monobank.ts (2)

881-885: monoLogInfo fires even when signature is still invalid after refresh.

The invalid_signature case is a security-relevant event (a request with an unverifiable signature even after key rotation) and arguably warrants monoLogWarn rather than monoLogInfo.

♻️ Suggested refactor — split log by result
-    monoLogInfo(MONO_PUBKEY_REFRESHED, {
-      endpoint: '/api/merchant/pubkey',
-      reason: 'refresh_once_after_verify_failed',
-      status: ok ? 'verified' : 'invalid_signature',
-    });
+    if (ok) {
+      monoLogInfo(MONO_PUBKEY_REFRESHED, {
+        endpoint: '/api/merchant/pubkey',
+        reason: 'refresh_once_after_verify_failed',
+        status: 'verified',
+      });
+    } else {
+      monoLogWarn(MONO_SIG_INVALID, {
+        endpoint: '/api/merchant/pubkey',
+        reason: 'invalid_signature_after_refresh',
+      });
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/lib/psp/monobank.ts` around lines 881 - 885, The log currently
always calls monoLogInfo with event MONO_PUBKEY_REFRESHED and sets status to ok
? 'verified' : 'invalid_signature'; change this to split the outcome so that
when ok is true you call monoLogInfo(MONO_PUBKEY_REFRESHED, { endpoint:
'/api/merchant/pubkey', reason: 'refresh_once_after_verify_failed', status:
'verified' }) but when ok is false call monoLogWarn(MONO_PUBKEY_REFRESHED, {
endpoint: '/api/merchant/pubkey', reason: 'refresh_once_after_verify_failed',
status: 'invalid_signature' }) so unverifiable-signature cases are logged as
warnings rather than info.

483-503: Module-level pubkey cache is instance-local — each serverless cold start will re-fetch.

_cachedWebhookKey is process-local. In Next.js serverless deployments (or any environment with ephemeral function instances), every cold start discards the cache and triggers a pubkey network call on the first webhook. Under high concurrency with frequent cold starts this can amplify calls to the Monobank pubkey endpoint.

Consider externalising the cache (e.g., Redis with a TTL matching PUBKEY_TTL_MS) if the deployment runs at significant scale, or at minimum document this behaviour for operators.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/lib/psp/monobank.ts` around lines 483 - 503, The module-level
variable _cachedWebhookKey used by getCachedWebhookKey() and cacheWebhookKey()
is process-local and will be lost on serverless cold starts causing repeated
pubkey fetches; modify the implementation to externalize the cache (e.g., use
Redis or another shared store) keyed by a stable name and set with a TTL
matching PUBKEY_TTL_MS, update getCachedWebhookKey() to read from the shared
store and cacheWebhookKey() to write the key+expiry into that store, and if you
cannot introduce a shared cache now, add clear documentation near PUBKEY_TTL_MS
and these functions explaining the process-local behavior and recommending
externalization for high-scale serverless deployments.
frontend/components/q&a/Pagination.tsx (1)

203-203: Consider replacing outline-none with outline-hidden for Tailwind v4 compatibility.

In Tailwind v4, outline-none was repurposed to set outline-style: none (removing the outline entirely), while outline-hidden provides the old v3 behavior—an invisible outline that remains visible in Windows High Contrast Mode / forced-colors. Since this line is being touched and the component already includes focus:bg-[var(--qa-accent-soft)] for sighted users, switching to outline-hidden ensures focus remains perceivable in forced-colors mode without breaking changes.

✏️ Suggested change to outline-hidden
-              className="min-w-20 appearance-none bg-transparent px-3 py-2 pr-9 text-sm font-medium text-gray-800 transition-colors outline-none hover:bg-[var(--qa-accent-soft)] focus:bg-[var(--qa-accent-soft)] dark:text-gray-200"
+              className="min-w-20 appearance-none bg-transparent px-3 py-2 pr-9 text-sm font-medium text-gray-800 transition-colors outline-hidden hover:bg-[var(--qa-accent-soft)] focus:bg-[var(--qa-accent-soft)] dark:text-gray-200"

Note: outline-none is used in 12+ locations across the codebase (frontend/lib/, frontend/components/). A broader sweep may be considered, but this pattern is common in Tailwind v4 projects with custom focus styling in place.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/q`&a/Pagination.tsx at line 203, The className in the
Pagination component uses Tailwind's deprecated outline-none which removes focus
outlines in forced-colors modes; change the utility to outline-hidden in the
className string inside the Pagination component (the class that currently
includes "outline-none" and "focus:bg-[var(--qa-accent-soft)]") so focus remains
perceivable in Windows High Contrast / forced-colors mode while keeping the
existing custom focus background behavior.
frontend/db/queries/users.ts (1)

67-68: Consider a nullish check instead of falsy for rankRow.rank.

!rankRow.rank treats 0 as absent. SQL RANK() always yields ≥ 1 so it's safe today, but a nullish guard is semantically clearer and more defensive if the query ever changes.

♻️ Proposed refactor
-  if (!rankRow || !rankRow.rank) {
+  if (!rankRow || rankRow.rank == null) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/db/queries/users.ts` around lines 67 - 68, The current guard treats
a zero rank as absent; update the condition that checks rankRow and rankRow.rank
so it only returns null when rankRow is missing or rankRow.rank is
null/undefined (i.e., use a nullish check for rankRow.rank rather than a falsy
check). Locate the check using the symbol rankRow and replace the falsy test on
rankRow.rank with an explicit null/undefined check (you can use optional
chaining or an equality comparison to null) so a numeric 0 would be preserved as
a valid rank.
frontend/app/[locale]/shop/products/[slug]/page.tsx (2)

27-28: Parallelize independent translation fetches to reduce TTFB.

The three await calls (getTranslations × 2 and getMessages) are fully independent and execute serially today, adding their latencies back-to-back on every SSR request.

♻️ Proposed refactor — parallel awaits
-  const t = await getTranslations('shop.products');
-  const tProduct = await getTranslations('shop.product');
+  const [t, tProduct, messages] = await Promise.all([
+    getTranslations('shop.products'),
+    getTranslations('shop.product'),
+    getMessages(),
+  ]);

   const currency = resolveCurrencyFromLocale(locale);
   // ...
-  const messages = await getMessages();

Also applies to: 57-57

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/`[locale]/shop/products/[slug]/page.tsx around lines 27 - 28,
The three independent awaits (getTranslations('shop.products') assigned to t,
getTranslations('shop.product') assigned to tProduct, and getMessages(...)
assigned to messages) are executed serially; change them to run in parallel
using Promise.all so they resolve concurrently (e.g., call Promise.all on the
three promise-returning calls and destructure the results back into t, tProduct,
messages). Apply the same pattern where similar independent awaits occur later
(referenced at the block around line 57) to reduce SSR TTFB.

15-18: Replace static metadata with generateMetadata for per-product SEO.

Every product slug currently renders with the same generic title and description. This defeats the purpose of a product detail page from an SEO standpoint.

♻️ Proposed refactor — dynamic metadata
-export const metadata: Metadata = {
-  title: 'Product name | DevLovers',
-  description: 'Details, price, and availability for product.',
-};
 export const dynamic = 'force-dynamic';

+export async function generateMetadata({
+  params,
+}: {
+  params: Promise<{ slug: string; locale: string }>;
+}): Promise<Metadata> {
+  const { slug, locale } = await params;
+  const currency = resolveCurrencyFromLocale(locale);
+  const product = await getPublicProductBySlug(slug, currency);
+  if (!product) return {};
+  return {
+    title: `${(product as any).name} | DevLovers`,
+    description: (product as any).description ?? 'Details, price, and availability for product.',
+  };
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/`[locale]/shop/products/[slug]/page.tsx around lines 15 - 18,
The static export const metadata should be replaced with an async export async
function generateMetadata({ params }): Promise<Metadata> that derives title and
description per product slug; inside generateMetadata use params.slug to fetch
the product (call your existing product-fetch helper or API), return { title:
`${product.name} | DevLovers`, description: product.description ?? 'Details,
price, and availability for product.' }, and provide a sensible fallback
title/description if the product fetch fails or product is not found.
frontend/components/home/InteractiveCTAButton.tsx (1)

3-9: Consider migrating from framer-motion to the canonical motion/react import path.

There are no breaking changes in Motion for React in version 12, but the recommended upgrade path is to uninstall framer-motion and install motion. All APIs used here (AnimatePresence, motion, useMotionTemplate, useMotionValue, useSpring) are available at the same names under motion/react.

♻️ Proposed import migration
-import {
-  AnimatePresence,
-  motion,
-  useMotionTemplate,
-  useMotionValue,
-  useSpring,
-} from 'framer-motion';
+import {
+  AnimatePresence,
+  motion,
+  useMotionTemplate,
+  useMotionValue,
+  useSpring,
+} from 'motion/react';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/home/InteractiveCTAButton.tsx` around lines 3 - 9, The
import currently pulls AnimatePresence, motion, useMotionTemplate,
useMotionValue, and useSpring from 'framer-motion'; update the module import to
'motion/react' so these symbols are imported from the canonical package (replace
the import statement that references AnimatePresence, motion, useMotionTemplate,
useMotionValue, useSpring). If you haven't already, uninstall the old
framer-motion package and install the new motion package so the new import
resolves correctly.
frontend/components/home/WelcomeHeroSection.tsx (1)

3-3: Migrate imports to motion/react consistently across all affected files.

The project imports framer-motion in at least 19 files. Given that framer-motion v12 is installed and this PR focuses on dependency upgrades, now is the right time to align with the official migration path. Motion's upgrade guide confirms no breaking changes in v12—this is a straightforward package and import path swap.

This requires two steps:

  1. Uninstall framer-motion and install the motion package
  2. Update all imports from framer-motion to motion/react

Affected files:

  • frontend/components/home/WelcomeHeroSection.tsx
  • frontend/components/shared/DynamicGridBackground.tsx
  • frontend/components/theme/ThemeToggle.tsx
  • frontend/components/home/FlipCardQA.tsx
  • frontend/components/home/InteractiveCTAButton.tsx
  • frontend/components/home/FloatingCode.tsx
  • frontend/components/leaderboard/LeaderboardClient.tsx
  • frontend/components/dashboard/AchievementsSection.tsx
  • frontend/components/dashboard/StatsCard.tsx
  • frontend/components/dashboard/ProfileCard.tsx
  • frontend/components/dashboard/ActivityHeatmapCard.tsx
  • frontend/components/dashboard/AchievementBadge.tsx
  • frontend/components/about/TopicsSection.tsx
  • frontend/components/about/SponsorsWall.tsx
  • frontend/components/about/PricingSection.tsx
  • frontend/components/about/HeroSection.tsx
  • frontend/components/about/InteractiveGame.tsx
  • frontend/components/about/FeaturesSection.tsx
  • frontend/components/leaderboard/LeaderboardPodium.tsx

All imports use the same API surface (motion, AnimatePresence, useMotionTemplate, useMotionValue, useReducedMotion) and are directly compatible with motion/react.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/home/WelcomeHeroSection.tsx` at line 3, Update the Framer
Motion imports to use the new package and path: replace imports from
'framer-motion' with 'motion/react' for symbols like motion, AnimatePresence,
useMotionTemplate, useMotionValue, and useReducedMotion (e.g., in
WelcomeHeroSection.tsx and the other listed components), and ensure the project
dependency is switched from framer-motion to the motion package (uninstall
framer-motion and install motion) so all components import from 'motion/react'
consistently.
frontend/components/admin/quiz/AnswerEditor.tsx (1)

48-48: Prefer focus:outline-hidden over focus:outline-none in Tailwind v4.

In Tailwind v4, outline-none only removes the outline-style property. As per the Tailwind v4 docs: "outline-none now only removes outline style. Use outline-hidden to remove the outline completely." The intended effect here is to suppress the browser's default focus outline while the custom focus:ring-* provides the visible indicator. focus:outline-hidden is the correct Tailwind v4 utility for this purpose.

♻️ Proposed fix
-        className="border-border bg-background text-foreground placeholder:text-muted-foreground flex-1 rounded-md border px-3 py-1.5 text-sm focus:ring-1 focus:ring-[var(--accent-primary)] focus:outline-none"
+        className="border-border bg-background text-foreground placeholder:text-muted-foreground flex-1 rounded-md border px-3 py-1.5 text-sm focus:ring-1 focus:ring-[var(--accent-primary)] focus:outline-hidden"

As per coding guidelines, the Tailwind v4 library documentation specifies: "outline-none now only removes outline style. Use outline-hidden to remove the outline completely."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/admin/quiz/AnswerEditor.tsx` at line 48, The Tailwind
class on the input in AnswerEditor.tsx uses focus:outline-none which in Tailwind
v4 only removes outline-style; replace it with focus:outline-hidden so the
browser focus outline is fully suppressed while keeping the custom focus:ring
(i.e., update the className string containing "focus:ring-1
focus:ring-[var(--accent-primary)] focus:outline-none" to use
"focus:outline-hidden" instead).
frontend/components/home/InteractiveConstellation.tsx (1)

146-347: icons object recreated every render but used inside useEffect — pre-existing, low priority.

The icons record (Line 40) is declared in the component body and captured by the useEffect closure, but it's not in the dependency array. Since icons has no reactive dependencies this works correctly, but if this component is ever refactored, consider moving icons inside the effect or extracting it to module scope to avoid the stale-closure footgun.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/home/InteractiveConstellation.tsx` around lines 146 -
347, The icons object is recreated every render and captured by the useEffect
closure in InteractiveConstellation, which can cause stale-closure bugs; fix by
either moving the icons declaration into the useEffect where draw/initParticles
use it, or extract icons to module scope (so it’s stable across renders), or
alternatively include icons in the useEffect dependency array; update references
to icons inside initParticles/draw accordingly to ensure the effect always sees
the correct, stable icons value.
frontend/components/auth/fields/EmailField.tsx (1)

28-43: Pre-existing: hardcoded English strings alongside translated messages.

Lines 30 and 41 use hardcoded English strings for tooShort/tooLong validation, while Lines 24 and 36 use t('validation.required') and t('validation.invalidEmail') from next-intl. Not introduced by this PR, but worth a follow-up to align all validation messages with the i18n system.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/auth/fields/EmailField.tsx` around lines 28 - 43, The
EmailField component currently sets hardcoded English messages for length
validation; replace those with i18n calls so all validation uses next-intl:
change the tooShort branch to input.setCustomValidity(t('validation.minLength',
{ min: minLength })) and the tooLong branch to
input.setCustomValidity(t('validation.maxLength', { max: maxLength })), ensuring
the component imports/has access to the t function already used for
t('validation.required') and t('validation.invalidEmail'), and keep existing
early returns and guards (minLength/maxLength) intact so behavior doesn't
change.
frontend/lib/github-stars.ts (1)

62-64: Optional: use project-level structured logging instead of console.warn/console.error.

Other server-side files (e.g., the admin quiz routes) use logWarn/logError from @/lib/logging. Replacing the raw console calls in this file would improve log consistency and ensure any structured-log enrichment (request-id, env, etc.) is applied uniformly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/lib/github-stars.ts` around lines 62 - 64, Replace the raw
console.warn/console.error calls in this file with the project-level structured
logger (import logWarn and logError from "@/lib/logging"), update the existing
console.warn(`⚠️ GitHub stargazers API error [${url}]: ${res.status}
${res.statusText}`) to call logWarn with the same message and any relevant
metadata (e.g., { url, status: res.status, statusText: res.statusText }), and
add imports at the top of the file; if there are other console calls in
functions like the GitHub stargazers fetcher, convert them similarly to use
logWarn/logError for consistent structured logging.
frontend/app/api/admin/quiz/[id]/questions/route.ts (1)

166-176: Pre-existing: hardcoded assumption of exactly 4 answers per question.

Not introduced by this PR, but worth noting: Math.floor(i / 4) and i % 4 on lines 168–169 assume every question has exactly 4 answers. If any question has a different answer count, the translation mapping will be incorrect. Consider computing the index dynamically based on actual answer counts if the schema doesn't enforce exactly 4.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/api/admin/quiz/`[id]/questions/route.ts around lines 166 - 176,
The mapping for inserting quizAnswerTranslations assumes exactly 4 answers per
question by using Math.floor(i / 4) and i % 4, which breaks if answer counts
vary; update the logic in the block that inserts into quizAnswerTranslations
(using insertedAnswers, questions, LOCALES) to compute qIdx and aIdx from the
actual structure—e.g., iterate questions and their answers to produce translated
rows or maintain a running counter of answersPerQuestion so quizAnswerId is
paired with the correct question and answer index rather than using a hardcoded
4; ensure the produced objects still include quizAnswerId, locale, and
answerText from questions[qIdx].answers[aIdx][locale].
frontend/db/queries/quizzes/quiz.ts (1)

25-29: Add a blank line before the export type block.

There is no blank line between the last import statement (ending at line 24) and the export type re-export, making it visually indistinguishable from the import list.

🔧 Suggested fix
 } from '../../schema/quiz';
+
 export type {
   QuizAnswer,
   QuizQuestion,
   QuizQuestionWithAnswers,
 } from '@/types/quiz';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/db/queries/quizzes/quiz.ts` around lines 25 - 29, Add a single blank
line immediately before the "export type { QuizAnswer, QuizQuestion,
QuizQuestionWithAnswers } from '@/types/quiz';" re-export so it is visually
separated from the preceding import list; locate the export type block (the
"export type" statement) and insert one empty line above it to improve
readability.
frontend/components/home/FlipCardQA.tsx (1)

358-358: Prefer focus-visible: variants over focus: for keyboard-only ring styling.

focus:outline-none removes the browser outline on both mouse clicks and keyboard navigation. The pattern focus-visible:ring-2 focus-visible:ring-(--accent-primary) focus-visible:ring-offset-2 focus-visible:outline-none limits the custom ring to keyboard focus only, which is the expected UX for dot-navigation controls.

🔧 Suggested fix
-            className="relative focus:ring-2 focus:ring-[var(--accent-primary)] focus:ring-offset-2 focus:outline-none"
+            className="relative focus-visible:ring-2 focus-visible:ring-(--accent-primary) focus-visible:ring-offset-2 focus-visible:outline-none"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/home/FlipCardQA.tsx` at line 358, In the FlipCardQA
component replace the generic focus: utility classes on the interactive element
(currently using className="... focus:ring-2 focus:ring-[var(--accent-primary)]
focus:ring-offset-2 focus:outline-none") with their focus-visible: equivalents
so the custom ring and outline removal apply only for keyboard focus; update the
class references to focus-visible:ring-2,
focus-visible:ring-[var(--accent-primary)], focus-visible:ring-offset-2 and
focus-visible:outline-none while removing or avoiding plain focus: variants.
frontend/components/dashboard/ProfileCard.tsx (1)

207-212: Pre-existing: change-name and change-password forms have stub onSubmit handlers.

Both handlers call setTimeout(() => setIsSaving(false), 1000) without sending any data to an API. The settings forms are visually complete but non-functional. This isn't introduced by this PR, but worth tracking as a follow-up.

Would you like me to open a tracking issue or draft the implementation for the name/password update API calls?

Also applies to: 243-251

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/dashboard/ProfileCard.tsx` around lines 207 - 212, The
change-name and change-password form onSubmit handlers currently only toggle
setIsSaving and use a stub setTimeout; replace these stubs with real async API
calls: call the appropriate profile update functions (e.g., an
updateName/updateProfile API or changePassword API), await the response,
validate inputs before sending, setIsSaving(true) before the call and
setIsSaving(false) in finally, update local state on success (e.g., setUser or
relevant state) and surface API errors to the user (set an error state or show
notifications) instead of silently hiding the spinner; remove the setTimeout and
ensure you reference the form handlers around the onSubmit in the ProfileCard
component so both the change-name and change-password blocks behave correctly.
frontend/components/admin/quiz/QuestionEditor.tsx (1)

402-402: Inconsistent CSS variable syntax — use Tailwind v4 parentheses shorthand.

Lines 402 and 449 use [var(--accent-primary)] (arbitrary-value bracket syntax), while the rest of the codebase uses the Tailwind v4 native -(--accent-primary) shorthand (e.g., statItemBase in ProfileCard.tsx, sponsor badge classes, etc.).

🔧 Suggested fix
-    <div className="rounded-lg border-2 border-[var(--accent-primary)]">
+    <div className="rounded-lg border-2 border-(--accent-primary)">
-            className="border-border bg-background text-foreground w-full rounded-md border px-3 py-2 text-sm focus:ring-1 focus:ring-[var(--accent-primary)] focus:outline-none"
+            className="border-border bg-background text-foreground w-full rounded-md border px-3 py-2 text-sm focus:ring-1 focus:ring-(--accent-primary) focus:outline-none"

Also applies to: 449-449

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/admin/quiz/QuestionEditor.tsx` at line 402, Replace the
inconsistent arbitrary-value Tailwind usage in QuestionEditor.tsx: find the divs
using className with "border-[var(--accent-primary)]" (e.g., the rounded-lg
border-2 element and the other occurrence around line 449) and update them to
the project's Tailwind v4 native CSS-variable shorthand (use
border-(--accent-primary) instead) so they match other usages like statItemBase
in ProfileCard.tsx and sponsor badge classes.
frontend/lib/validation/quiz-publish-validation.ts (1)

12-12: Import locales from frontend/i18n/config.ts instead of hardcoding.

The local constant const LOCALES = ['en', 'uk', 'pl'] duplicates the locales export from frontend/i18n/config.ts. Importing it ensures locale changes are reflected automatically.

♻️ Suggested refactor
-const LOCALES = ['en', 'uk', 'pl'];
+import { locales as LOCALES } from '@/i18n/config';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/lib/validation/quiz-publish-validation.ts` at line 12, Replace the
hardcoded LOCALES array in quiz-publish-validation.ts with the canonical locales
export from frontend/i18n/config.ts: remove const LOCALES = ['en','uk','pl'] and
instead import { locales } (or the exported name) from that config file, then
use that imported symbol wherever LOCALES is referenced (e.g., validation
routines in this module) so locale changes in the config propagate
automatically.
frontend/components/dashboard/ActivityHeatmapCard.tsx (2)

82-86: Both branches of this conditional are identical.

The if (periodOffset === 0) and else blocks both execute the same setDate(getDate() + 1) call. This dead conditional can be simplified.

Suggested simplification
-    if (periodOffset === 0) {
-      windowEndExclusive.setDate(windowEndExclusive.getDate() + 1);
-    } else {
-      windowEndExclusive.setDate(windowEndExclusive.getDate() + 1);
-    }
+    windowEndExclusive.setDate(windowEndExclusive.getDate() + 1);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/dashboard/ActivityHeatmapCard.tsx` around lines 82 - 86,
The conditional in ActivityHeatmapCard.tsx around periodOffset is dead code
because both branches call
windowEndExclusive.setDate(windowEndExclusive.getDate() + 1);; remove the
if/else and replace with a single unconditional call to
windowEndExclusive.setDate(windowEndExclusive.getDate() + 1); where the current
conditional lives to simplify logic and avoid redundant code (search for
periodOffset and windowEndExclusive in the file to locate the exact spot).

188-201: Unused streak and max variables inside useMemo.

The memo computes streak and max but only returns todayKey. The streak/max computation is dead code here (streak display uses the currentStreak prop instead).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/dashboard/ActivityHeatmapCard.tsx` around lines 188 -
201, The useMemo in ActivityHeatmapCard calculates unused variables streak and
max by iterating heatmapData but only returns todayKey; remove the dead loop and
variables and simplify the useMemo to only compute and return todayKey (keeping
heatmapData in the dependency array), or if you intended to use streak/max
instead, return them along with todayKey and update the component to consume
currentStreak/currentMax; update the useMemo around todayKey, streak, and max
accordingly.
frontend/app/[locale]/quiz/[slug]/page.tsx (1)

8-12: Duplicate import source: two imports from '@/db/queries/quizzes/quiz'.

stripCorrectAnswers (Line 8) and getQuizBySlug/getQuizQuestionsRandomized (Lines 9-12) both import from the same module. This works at runtime but could trigger linter warnings and is likely an artifact of the reformatting. Consider consolidating into a single import statement.

Suggested consolidation
-import { stripCorrectAnswers } from '@/db/queries/quizzes/quiz';
-import {
-  getQuizBySlug,
-  getQuizQuestionsRandomized,
-} from '@/db/queries/quizzes/quiz';
+import {
+  getQuizBySlug,
+  getQuizQuestionsRandomized,
+  stripCorrectAnswers,
+} from '@/db/queries/quizzes/quiz';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/`[locale]/quiz/[slug]/page.tsx around lines 8 - 12, Consolidate
the duplicate imports from '@/db/queries/quizzes/quiz' into a single statement:
replace the two import blocks with one import that imports stripCorrectAnswers,
getQuizBySlug, and getQuizQuestionsRandomized together; update the import that
currently references stripCorrectAnswers and the import that references
getQuizBySlug/getQuizQuestionsRandomized so only one combined import remains
(referencing the symbols stripCorrectAnswers, getQuizBySlug,
getQuizQuestionsRandomized).
frontend/app/[locale]/admin/shop/orders/page.tsx (1)

65-80: Unnecessary optional chaining on order in viewModels mapping.

order is an element from items.map(...) — it can never be null/undefined. The ?. on lines 67 is superfluous.

Suggested fix
-    const totalMinor = pickMinor(order?.totalAmountMinor, order?.totalAmount);
+    const totalMinor = pickMinor(order.totalAmountMinor, order.totalAmount);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/`[locale]/admin/shop/orders/page.tsx around lines 65 - 80, The
map callback creates viewModels from each order item so the optional chaining on
the local variable is unnecessary; remove the `?.` usage when calling pickMinor
and when accessing order.totalAmount (i.e., inside the mapping where
pickMinor(order?.totalAmountMinor, order?.totalAmount) is used) and keep plain
property access like pickMinor(order.totalAmountMinor, order.totalAmount); this
is in the mapping that defines viewModels (referencing orderCurrency,
formatDate, pickMinor, formatMoney, and t).
frontend/app/[locale]/admin/shop/orders/[id]/page.tsx (1)

20-31: Extract pickMinor and orderCurrency into a shared utility module.

pickMinor is duplicated identically across frontend/app/[locale]/admin/shop/orders/page.tsx (lines 26–29) and frontend/app/[locale]/admin/shop/orders/[id]/page.tsx (lines 20–23). orderCurrency is also duplicated with functionally identical logic, though with different parameter type signatures. Extract both into a shared utility (e.g., @/lib/shop/order-utils.ts) and update type signatures to accept the more flexible union type to serve both call sites.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/`[locale]/admin/shop/orders/[id]/page.tsx around lines 20 - 31,
Extract the duplicated functions pickMinor and orderCurrency into a shared
utility module (e.g., create a new module exporting pickMinor and
orderCurrency), update their signatures to be flexible (pickMinor(minor:
unknown, legacyMajor: unknown): number | null and orderCurrency(order: {
currency?: string | null } | Partial<{currency?: string|null}>, locale: string):
CurrencyCode or otherwise accept the union types used at both call sites),
replace the duplicate implementations in both order pages with imports from the
new utility, and remove the old local copies; ensure callers still import from
the new module and that orderCurrency uses resolveCurrencyFromLocale and the
same UAH/USD logic.

Comment on lines +64 to +85
<main className="mx-auto max-w-6xl px-4 py-8" aria-labelledby="order-title">
<header className="flex items-start justify-between gap-4">
<div className="min-w-0">
<h1 id="order-title" className="text-foreground text-2xl font-bold">
Order
</h1>
<p className="text-muted-foreground mt-1 font-mono text-xs break-all">
{order.id}
</p>
</div>

<div className="flex shrink-0 flex-wrap items-center gap-2">
<Link
href="/admin/shop/orders"
className="border-border text-foreground hover:bg-secondary rounded-md border px-3 py-1.5 text-sm font-medium transition-colors"
>
Back
</Link>

<main
className="mx-auto max-w-6xl px-4 py-8"
aria-labelledby="order-title"
<RefundButton orderId={order.id} disabled={!canRefund} />
</div>
</header>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hardcoded English strings throughout admin order detail page.

The sibling orders list page (orders/page.tsx) uses t('title'), t('table.*'), etc. for all user-facing text, but this detail page hardcodes "Order", "Back", "Summary", "Payment status", "Total", "Provider", "Payment intent", "Idempotency key", "Stock / timestamps", "Created", "Updated", "Stock restored", "Product", "Qty", "Unit", "Line total", "No items found…", and "Order items".

This creates an i18n gap — the page won't be translatable for non-English locales. Consider extracting these to a translation namespace (e.g. shop.admin.orderDetail.*), consistent with the pattern used in the list page.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/`[locale]/admin/shop/orders/[id]/page.tsx around lines 64 - 85,
The page.tsx admin order detail component hardcodes all user-facing strings
("Order", "Back", "Summary", etc.), breaking i18n; replace these literals with
calls to the app's translation function (e.g.,
t('shop.admin.orderDetail.title'), t('shop.admin.orderDetail.back'), etc.) and
load the same translation namespace used by the sibling orders list (ensure you
import/use the translation hook or server-side t loader used across admin
pages), updating all occurrences in the component (header text, buttons like
RefundButton label if present, section headings like "Summary", "Payment
status", "Stock / timestamps", table headers "Product", "Qty", "Unit", "Line
total", "No items found…", and "Order items") to use keys under a consistent
namespace such as shop.admin.orderDetail.*.

Comment on lines 54 to +55
const passwordBytesTooLong =
passwordTouched &&
utf8ByteLength(passwordValue) > PASSWORD_MAX_BYTES;
passwordTouched && utf8ByteLength(passwordValue) > PASSWORD_MAX_BYTES;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Two error messages render simultaneously when the password exceeds the byte limit.

When passwordBytesTooLong is true, passwordPolicyOk is also false (line 40 catches the same condition), so both passwordErrorText (the generic "invalid password" paragraph at line 116) and the specific passwordTooLongBytes paragraph (lines 120-124) are visible at the same time. Confirm this double-display is intentional; if not, suppress passwordErrorText when the only failure reason is byte length.

💡 One way to suppress the generic message when bytes are the only failure
-  const passwordErrorText =
-    passwordTouched && !passwordPolicyOk
-      ? tf('validation.invalidPassword', { passwordRequirementsText })
-      : null;
+  const passwordBytesTooLong =
+    passwordTouched && utf8ByteLength(passwordValue) > PASSWORD_MAX_BYTES;
+
+  const passwordErrorText =
+    passwordTouched && !passwordPolicyOk && !passwordBytesTooLong
+      ? tf('validation.invalidPassword', { passwordRequirementsText })
+      : null;

Also applies to: 115-125

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/auth/ResetPasswordForm.tsx` around lines 54 - 55, The
generic and specific password error messages both render when the password
exceeds byte limit; to fix, change the rendering condition for the generic
`passwordErrorText` so it is suppressed when `passwordBytesTooLong` is the only
failing condition. In practice, update the JSX/conditional that renders
`passwordErrorText` (the paragraph using `passwordPolicyOk`) to require
`passwordTouched && !passwordPolicyOk && !passwordBytesTooLong` (or equivalent)
so `passwordTooLongBytes` (the specific message driven by
`passwordBytesTooLong`, `PASSWORD_MAX_BYTES`, `utf8ByteLength`, and
`passwordTouched`) displays alone when byte-length is the sole failure.

Comment on lines +615 to +617
{tooltip.count === 0
? 'No activity'
: `${tooltip.count} ${tooltip.count === 1 ? 'attempt' : 'attempts'}`}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hard-coded English strings bypass the i18n system.

The tooltip text "No activity", "attempt", and "attempts" are not using useTranslations. Similarly, "active day" / "active days" on Line 661 are hard-coded. All other user-facing text in this component correctly uses t().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/dashboard/ActivityHeatmapCard.tsx` around lines 615 -
617, Replace the three hard-coded tooltip strings in ActivityHeatmapCard with
translations via the existing useTranslations hook: change "No activity" to
t('...') (e.g., t('noActivity')), and replace singular/plural
"attempt"/"attempts" and "active day"/"active days" with pluralized calls (e.g.,
t.rich or t with count/vars) so the component uses t(count, { count }) or an
appropriate plural key; update the translation keys rather than leaving literal
strings so all user-facing text uses the t() function in the ActivityHeatmapCard
render/tooltip logic.

const [currentText, setCurrentText] = useState(t('cta'));
const [variantIndex, setVariantIndex] = useState(0);
const [variantIndex, setVariantIndex] = useState(0);
const [isFirstRender, setIsFirstRender] = useState(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove the unused isFirstRender state — it's dead code that triggers an unnecessary re-render on mount.

isFirstRender is initialised to true, flipped to false inside a mount-only useEffect, and never read anywhere in the component. The only effect is an extra state update (and re-render) on every mount.

🐛 Proposed fix
-    const [isFirstRender, setIsFirstRender] = useState(true);
-    useEffect(() => {
-      setIsFirstRender(false);
-    }, []);
-

Also applies to: 38-40

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/home/InteractiveCTAButton.tsx` at line 24, Remove the
dead state used for tracking initial render in the InteractiveCTAButton
component: delete the useState call that declares isFirstRender and
setIsFirstRender and remove the mount-only useEffect that flips it (references
to useState/useEffect and the identifiers isFirstRender/setIsFirstRender).
Ensure no other code reads isFirstRender before removing; if anything depends on
initial-mount behavior, replace it with a ref (useRef) or logic that does not
cause a state update on mount. Also remove any duplicate unused occurrences of
the same pattern elsewhere in the component (the other
isFirstRender/setIsFirstRender usage).

<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down motion-reduce:!animate-none overflow-hidden text-sm"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm motion-reduce:!animate-none"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and check the tailwindcss version in the project
find . -name "package.json" -o -name "package-lock.json" -o -name "yarn.lock" -o -name "pnpm-lock.yaml" | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 172


🌐 Web query:

Tailwind CSS v4 important modifier syntax prefix vs suffix breaking change

💡 Result:

In Tailwind CSS v4, the important modifier moved from a prefix to a suffix:

  • v3 (old / now deprecated in v4): hover:!bg-red-500
  • v4 (preferred): hover:bg-red-500! (tailwindcss.com)

So, the ! now goes at the very end of the full class, after the utility (and after any arbitrary values/modifiers), e.g.:

<div class="flex! bg-red-500! hover:bg-red-600/50!"></div>
``` ([tailwindcss.com](https://tailwindcss.com/docs/upgrade-guide?utm_source=openai))

The **old prefix form still works for compatibility but is deprecated** in v4. ([tailwindcss.com](https://tailwindcss.com/docs/upgrade-guide?utm_source=openai))

Citations:

- 1: https://tailwindcss.com/docs/upgrade-guide?utm_source=openai
- 2: https://tailwindcss.com/docs/upgrade-guide?utm_source=openai
- 3: https://tailwindcss.com/docs/upgrade-guide?utm_source=openai

---

🌐 Web query:

`Tailwind CSS v4 ! modifier position animate-none motion-reduce`

💡 Result:

In **Tailwind CSS v4**, the **important (`!`) modifier goes at the very end of the class name** (after the utility and after any variants). The old v3-style `!`-prefix is still supported but **deprecated**. [1]

So for reduced-motion animation:

- Normal reduced-motion variant (no `!important`):  
  `motion-reduce:animate-none` (uses the `motion-reduce` variant + `animate-none`). [2]

- Reduced-motion + `!important` in v4:  
  `motion-reduce:animate-none!` [1]

- Deprecated v3-style equivalent (avoid in v4):  
  `motion-reduce:!animate-none` [1]

**Examples**
```html
<!-- Disable animation when the user prefers reduced motion -->
<div class="motion-reduce:animate-none"></div>

<!-- Force it with !important (v4 syntax) -->
<div class="motion-reduce:animate-none!"></div>

Sources: Tailwind upgrade guide (important modifier change) [1], Tailwind animation docs (motion-reduce usage, animate-none) [2], Tailwind Labs discussion confirming postfix ! as the new standard [3].


🏁 Script executed:

# Check tailwindcss version in frontend package.json
cat frontend/package.json | grep -A 2 -B 2 tailwindcss

Repository: DevLoversTeam/devlovers.net

Length of output: 414


🏁 Script executed:

# Verify the actual code at the mentioned line
cat -n frontend/components/ui/accordion.tsx | sed -n '55,70p'

Repository: DevLoversTeam/devlovers.net

Length of output: 696


Update to Tailwind v4 important modifier syntax: motion-reduce:animate-none!

The project uses Tailwind CSS v4. The important modifier (!) syntax changed in v4 — it must now be a suffix, not a prefix:

v3 (deprecated in v4) v4 (current best practice)
!animate-none animate-none!

While Tailwind v4 still supports the old prefix syntax for backward compatibility, it is deprecated. Update this line to follow v4 conventions:

Proposed fix
-      className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm motion-reduce:!animate-none"
+      className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm motion-reduce:animate-none!"

This ensures the animation override respects prefers-reduced-motion: reduce per WCAG 2.1 §2.3.3.

📝 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
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm motion-reduce:!animate-none"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm motion-reduce:animate-none!"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/ui/accordion.tsx` at line 61, Replace the deprecated
Tailwind important-prefix syntax in the className on the accordion content
element: change the modifier "motion-reduce:!animate-none" to the v4 suffix form
"motion-reduce:animate-none!" in frontend/components/ui/accordion.tsx (look for
the className string containing data-[state=closed]:animate-accordion-up
data-[state=open]:animate-accordion-down overflow-hidden text-sm
motion-reduce:!animate-none).

@ViktorSvertoka ViktorSvertoka merged commit c5d3875 into develop Feb 22, 2026
12 checks passed
@ViktorSvertoka ViktorSvertoka deleted the fix/refactoring branch February 22, 2026 15:55
@coderabbitai coderabbitai bot mentioned this pull request Mar 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working refactor Code restructuring without functional changes setup Configuration, tooling, infrastructure setup

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant