Skip to content

Release v0.5.4#278

Merged
ViktorSvertoka merged 94 commits intomainfrom
develop
Feb 5, 2026
Merged

Release v0.5.4#278
ViktorSvertoka merged 94 commits intomainfrom
develop

Conversation

@ViktorSvertoka
Copy link
Member

@ViktorSvertoka ViktorSvertoka commented Feb 5, 2026

Release v0.5.4 - performance, SEO, UX and accessibility improvements

Summary by CodeRabbit

  • New Features

    • Added quiz SEO metadata generation for improved search visibility
    • Introduced dynamic grid background visual
  • Bug Fixes

    • Fixed mobile anti-cheat false positives
    • Resolved timer synchronization issues
    • Improved mobile contrast and touch responsiveness
    • Corrected mobile spacing and padding inconsistencies
    • Enhanced page load performance by removing render-blocking font resources
  • Improvements

    • Enhanced accessibility coverage across the app
    • Refined Q&A pagination and mobile behavior
    • Optimized cart with stock-aware quantity controls
    • Polished UI styling and visual consistency
    • Improved multilingual support with refined translations

LesiaUKR and others added 30 commits January 24, 2026 16:26
…(issues #181, #193, #194)

- Refactor QaTabButton to shared CategoryTabButton component
- Add category accent colors to QuizCard, buttons, progress indicators
- Standardize colors with CSS variables, traffic light timer
- Add DynamicGridBackground to quizzes list page
- Border-only answer feedback, semi-transparent progress styles
Changed violationsCount > 3 to >= 3 in QuizResult points block
to match the warning banner threshold at line 124.
- Configure Vitest for quiz module
- Add test factories and setup utilities
- Add quiz-crypto tests (13 tests)
- Add quiz-session tests (12 tests)
…eat hook (#199)

- verify-answer.test.ts: 8 tests for API endpoint
  - Correct/wrong answer verification
  - Validation errors (missing fields, tampered data)
  - Security: rejects modified encrypted answers

- quiz-anticheat.test.ts: 10 tests for useAntiCheat hook
  - Detects copy, paste, context-menu, tab-switch events
  - Respects isActive flag
  - Reset and cleanup functionality

Total quiz tests: 52 (9 setup + 25 unit + 18 integration)
…UI flow

Add 28 new tests covering:
- useQuizSession hook (6 tests)
- useQuizGuards hook (8 tests)
- guest-quiz storage (5 tests)
- guest-result API route (5 tests)
- quiz-slug API route (3 tests)
- QuizContainer UI flow (1 test)

Coverage: 35% -> 90.94% (quiz scope)
Tests: 52 -> 80
* feat(md) add netlify status

* feat(files): add packages

* fix(auth): use currentTarget for email input validity
* feat(md) add netlify status

* feat(files): add packages

* fix(auth): use currentTarget for email input validity

* fix(auth): use currentTarget for email input validity

* fix(auth): use currentTarget in password field
* (SP 2) [Shop UI] Unify storefront styles across components and interactions

* (SP 1) [Shop UI] Add page metadata across shop routes
* feat(md) add netlify status

* feat(files): add packages

* fix(auth): use currentTarget for email input validity

* fix(auth): use currentTarget for email input validity

* fix(auth): use currentTarget in password field

* feat(qa): add Redis cache layer for Q&A
* feat(md) add netlify status

* feat(files): add packages

* fix(auth): use currentTarget for email input validity

* fix(auth): use currentTarget for email input validity

* fix(auth): use currentTarget in password field

* feat(qa): add Redis cache layer for Q&A

* fix(qa): handle non-string Redis cache values
* feat(Blog):fix for clickable link in post details, fix for author details

* feat(Blog):refactoring after removing author modal

* feat(Blog): fix unified date format

* feat(Blog): Fix for  click-outside-to-close search, recommended posts are limited to 3

* feat(Blog): selectedAuthorData fixed

* feat(Blog): Added description for /blog/[slug] metadata, Added Schema.org JSON‑LD for Article (BlogPosting) and BreadcrumbList , Added <time datetime> tags where blog dates renders

* feat(Blog): fix hover social links, fixed duplication not found search

* feat(Blog): Added: breadcrumbs to the post details page and updated the BreadcrumbList, logo to the cocial links in User info, Fixed: main container alignment, category navigation in breadcrumbs

* feat(Blog): Added: breadcrumbs to the post details page and updated the BreadcrumbList, logo to the cocial links in User info, Fixed: main container alignment, category navigation in breadcrumbs

* feat(Blog): Added scroll on the main blog page on filtering by author, fied breadcrumbs category translaion, added category to the recommended cards, fixed search for localisations

* feat(Blog): Changed image size on the post details page

* feat(Blog): added tests

* feat(Blog): fix for big post on the post page, added tests

* feat(Blog): resolving comments

* feat(Blog): fixed hover for social links icins - dark theme

* feat(Blog): bringing the style on the blog page to a single site style

* feat(blog): aligning syles

* feat(blog): resolving comment from CodeRabbit

* feat(blog):fix comment for deployment

* feat(Blog): adding pagination

* feat(Blog): Addind Text formatting visibility
)

- Refactor /api/ai/explain route for Vercel deployment
    - Replace dynamic import with static import of groq-sdk
    - Use request.json() instead of Netlify-safe body parsing
    - Add proper error handling with Groq.APIError types
    - Simplify GET health check endpoint
    - Update model from llama3-70b-8192 to llama-3.3-70b-versatile
- Add table.openOrder and table.orderId to en/uk/pl locales
Improve primary button styles
Fix card overflow and add subtle highlights
Redesign online users counter and reduce font size
LesiaUKR and others added 16 commits February 4, 2026 23:25
- Add touch detection to anti-cheat to prevent false contextmenu violations on mobile
- Add select-none and webkit-touch-callout CSS to prevent text selection
- Fix timer progress bar jump on navigation using isSynced state
- Fix TypeScript error in quiz metadata description
- Reset isSynced state when endTime changes to prevent transition jump
- Reset isSynced on tab visibility change for instant sync
- Add --force flag to JS quiz seed for reseeding with existing attempts
- Expand JS Fundamentals quiz from 10 to 40 questions
(SP: 1) [Frontend] Improve Lighthouse Performance and SEO scores for quiz pages
(SP 1) [Shop] Canonicalize legacy filters + fix cart CTA/badge UX
…on-table-contrast

(SP: 1) [Frontend] Fix Q&A mobile tap lock, pagination scroll, and table text contrast
  - Replace dashboard background with DynamicGridBackground
  - Fix suggestCanonicalClasses warnings across multiple components
  Changes:
  - LanguageSwitcher.tsx: Replace z-60 with valid z-50
  - en.json: Fix capitalization after commas
style: update dashboard UI and fix canonical Tailwind v4 classes
@ViktorSvertoka ViktorSvertoka self-assigned this Feb 5, 2026
@ViktorSvertoka ViktorSvertoka added bug Something isn't working performance Performance and efficiency optimizations without functional changes. refactor Code restructuring without functional changes labels Feb 5, 2026
@vercel
Copy link
Contributor

vercel bot commented Feb 5, 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 5, 2026 4:56pm

@netlify
Copy link

netlify bot commented Feb 5, 2026

Deploy Preview for develop-devlovers ready!

Name Link
🔨 Latest commit 9c0a867
🔍 Latest deploy log https://app.netlify.com/projects/develop-devlovers/deploys/6984cb8adeff330008fd7315
😎 Deploy Preview https://deploy-preview-278--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 5, 2026

📝 Walkthrough

Walkthrough

This PR introduces version 0.5.3 and 0.5.4 releases with extensive UI polish, styling token standardization, internationalization enhancements, quiz metadata generation, accessibility improvements, anti-cheat refinements, and component API refactoring across the frontend.

Changes

Cohort / File(s) Summary
Quiz Metadata & SEO
frontend/app/[locale]/quiz/[slug]/page.tsx, frontend/app/[locale]/quizzes/page.tsx
Added server-side metadata generation with locale-aware translations for dynamic page titles and descriptions.
Dashboard Layout Refactoring
frontend/app/[locale]/dashboard/page.tsx, frontend/app/[locale]/q&a/page.tsx
Replaced decorative background markup with DynamicGridBackground wrapper; adjusted layout structure and styling tokens for improved visual hierarchy.
Styling Token Updates
frontend/app/[locale]/shop/page.tsx, frontend/components/q&a/AIWordHelper.tsx, frontend/components/q&a/FloatingExplainButton.tsx, frontend/components/shared/LanguageSwitcher.tsx, frontend/app/not-found.tsx
Standardized CSS variable syntax from [var(--accent-primary)] to (--accent-primary) and updated gradient utilities from bg-gradient-to-r to bg-linear-to-r across multiple components.
Component Style Refinements
frontend/components/dashboard/ProfileCard.tsx, frontend/components/dashboard/StatsCard.tsx, frontend/components/dashboard/QuizSavedBanner.tsx
Updated color tokens, border styles, backdrop blur, and typography to use neutral/gray palette with dark mode variants; simplified badge and section styling.
Home/Hero Components Refactoring
frontend/components/home/InteractiveCTAButton.tsx, frontend/components/home/HeroSection.tsx, frontend/components/shared/OnlineCounterPopup.tsx, frontend/components/shared/Footer.tsx
Refactored InteractiveCTAButton to forwardRef with dynamic text on hover; integrated OnlineCounterPopup with ref-based positioning logic; added ref props to Footer and HeroSection for external DOM access.
Blog Components Updates
frontend/app/[locale]/blog/[slug]/PostDetails.tsx, frontend/components/blog/BlogFilters.tsx, frontend/components/blog/BlogHeaderSearch.tsx
Replaced img with Next.js Image component; simplified pagination state management; added locale dependency for search re-fetching; removed unused linkKey computation.
Q&A Components Enhancements
frontend/components/q&a/QaSection.tsx, frontend/components/q&a/AccordionList.tsx, frontend/components/q&a/useQaTabs.ts
Added section-level scroll-on-page-change behavior with text selection clearing; introduced clearSelection on accordion expand; removed automatic smooth scroll from page navigation.
Quiz Timer & Anti-Cheat
frontend/components/quiz/CountdownTimer.tsx, frontend/hooks/useAntiCheat.ts
Stabilized timer synchronization with isSynced state and visibility resets; replaced clock emoji with Clock icon; added touch interaction tracking to prevent context-menu false positives.
Shop Features
frontend/app/[locale]/shop/products/page.tsx, frontend/app/api/shop/catalog/route.ts, frontend/components/shop/AddToCartButton.tsx, frontend/components/shop/CartPageClient.tsx, frontend/components/shop/ProductCard.tsx, frontend/components/shop/ProductSort.tsx
Added legacy filter canonicalization (filter=new → sort=newest); implemented stock-aware quantity limiting; added checkout spinner with loading state; unified badge styling; added peer class to sort select.
Image & Responsive Updates
frontend/components/home/CodeCard.tsx, frontend/components/home/HeroCodeCards.tsx, frontend/app/globals.css
Added responsive breakpoints (lg/xl) for CodeCard sizing and positioning; extended animation utilities for xl breakpoint; removed Font Awesome CSS import; expanded .no-select rule for vendor prefixes.
Internationalization
frontend/messages/en.json, frontend/messages/pl.json, frontend/messages/uk.json
Added new quiz metadata keys (notFoundTitle, metaSuffix, metaDescriptionFallback, metaTitle, metaDescription); updated shop copy with stock and maxStock labels; normalized punctuation across translations.
Database Seeding
frontend/db/seed-quiz-javascript.ts
Added --force flag support for deleting existing quiz attempts; updated ensureQuizExists to accept forceDelete parameter; improved CLI help and warning messaging.
Layout & Component Cleanup
frontend/app/[locale]/layout.tsx, frontend/components/shared/HeaderButton.tsx
Removed OnlineCounterPopup from main layout; added z-index adjustments to HeaderButton badge layering.
Testing Updates
frontend/components/tests/blog/blog-card.test.tsx, frontend/components/tests/blog/blog-filters.test.tsx, frontend/components/tests/blog/blog-header-search.test.tsx, frontend/components/tests/q&a/use-qa-tabs.test.tsx, frontend/lib/tests/quiz/quiz-crypto.test.ts
Updated blog card mock to return img with explicit alt; removed unused beforeEach imports; removed scrollTo assertion from tab tests; added comprehensive crypto encryption/decryption test suite.
Configuration & Version Updates
frontend/.browserslistrc, frontend/package.json, studio/package.json
Added browserslist configuration for ES6 module support; bumped versions from 0.5.2 to 0.5.4.
Removed Exports
frontend/lib/home/cta-variants.ts
Removed CTAVariant interface and createCTAVariants function (52 lines); functionality moved inline or eliminated.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

UI, enhancement, i18n, accessibility

Suggested reviewers

  • AM1007

Poem

🐰 Hops through tokens and themes with care,
DynamicGrids floating in the air,
Refs now forward, scrolls now smooth,
With anti-cheat touch to soothe,
A rabbit's polish, shining bright!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.50% 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 'Release v0.5.4' clearly and concisely summarizes the main change: a version release. It is specific, directly related to the changeset (CHANGELOG and package.json updates), and a teammate scanning history would immediately understand this is a release commit.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

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: 9

🤖 Fix all issues with AI agents
In `@frontend/app/`[locale]/dashboard/page.tsx:
- Line 99: The support item is rendered as a non-interactive <span> with the
class outlineBtnStyles and translation key supportLink; if it should be a
clickable link, replace the <span> with a proper interactive element (e.g.,
Next.js <Link> or an <a> with the correct href) and preserve outlineBtnStyles
and the t('supportLink') text, or if it is purely decorative add
aria-hidden="true" and/or rename the translation key (supportBadge) to avoid
implying interactivity; update the element where t('supportLink') is used in
page.tsx accordingly.

In `@frontend/app/`[locale]/shop/page.tsx:
- Around line 54-57: Update the Link that renders the "View All" button so its
query param uses the modern sort key: change the href on the Link component (the
JSX starting with <Link href="/shop/products?filter=newest" ...>) to use
/shop/products?sort=newest instead of filter=newest so the products page and
catalog API receive the supported sort=newest parameter.

In `@frontend/components/blog/BlogHeaderSearch.tsx`:
- Line 98: The effect in BlogHeaderSearch that depends on [open, items.length,
isLoading, locale] is still guarded by if (!open || items.length || !isLoading)
return, so switching locale leaves stale items in place; fix by clearing cached
results when locale changes—either add a separate useEffect that watches locale
and calls setItems([])/clearItems(), or modify the existing effect to treat a
locale change as invalidating items (e.g., check previous locale and call
setItems([]) before the guard) so reopening after a locale switch triggers a
fresh fetch.

In `@frontend/components/q`&a/AccordionList.tsx:
- Line 247: Change the table's dark-mode text class so text remains readable in
dark backgrounds by replacing or removing "dark:text-gray-900" (e.g., use
"dark:text-gray-100" or remove the dark override) in the table element's
className, and update the row background classes "bg-white" and "bg-gray-50" on
the table rows to include dark variants (e.g., "dark:bg-gray-800" /
"dark:bg-gray-700" or similar) so row contrast in dark mode matches the new text
color.

In `@frontend/components/quiz/CountdownTimer.tsx`:
- Around line 37-48: The local synced boolean in the interval closure (variable
synced) doesn't reflect visibility-driven resets; replace it with a React ref
(e.g., syncedRef = useRef(false)) and use syncedRef.current inside the
setInterval callback instead of the local variable, initialize/reset
syncedRef.current = false at the start of the effect, and also reset
syncedRef.current = false inside the visibility/change handler whenever you call
setIsSynced(false) so the sync logic (setIsSynced(true)) can run again after the
tab becomes visible; ensure the interval cleanup still clears and that you
update any references to synced to use the ref (syncedRef.current).
- Around line 24-32: The reset logic that calls setRemainingSeconds, setIsSynced
and setPrevEndTime during render must be moved into a useEffect to avoid
setState during render; remove the prevEndTime state entirely and add a
useEffect that watches [endTime, timeLimitSeconds] and when endTime changes
reset remainingSeconds (via setRemainingSeconds(timeLimitSeconds)) and
setIsSynced(false); ensure no state updates occur in the render body and
reference the existing state setters remainingSeconds/setRemainingSeconds and
isSynced/setIsSynced and the props endTime and timeLimitSeconds inside that
effect.

In `@frontend/components/shared/OnlineCounterPopup.tsx`:
- Around line 34-77: The mobile top calculation in calculatePosition and
handleResize incorrectly adds window.scrollY (and duplicatively rect.height)
even though the popup uses position: fixed; update both functions to compute
viewport-relative values: use rect.bottom + 14 (do not add rect.height or
window.scrollY) as the desired top, compute max as window.innerHeight -
popupHeight - safeBottom, then set newTop = Math.min(desired, max); keep
references to ctaRef and setPosition as-is and apply the same change in both
calculatePosition and handleResize.

In `@frontend/components/shop/AddToCartButton.tsx`:
- Around line 45-54: The ShopProduct type lacks the stock field causing a cast
to any in AddToCartButton.tsx; update shopProductSchema in
frontend/lib/validation/shop.ts to include the stock property (matching the
shape in dbProductSchema, e.g., number or nullable/optional number as
appropriate) so that the generated ShopProduct type includes stock and you can
remove the unsafe (product as any).stock usage in AddToCartButton (which
computes stockRaw/stockNum/maxQty).

In `@frontend/hooks/useAntiCheat.ts`:
- Line 25: Replace the boolean ref lastInteractionWasTouch with a timestamp ref
(e.g., lastTouchTimestamp = useRef<number>(0)) and update all
touch/mouse/contextmenu handlers to use a short suppression window (suggest
~800ms): set lastTouchTimestamp.current = Date.now() on touchstart/touchend, and
in the contextmenu handler ignore/suppress violations if Date.now() -
lastTouchTimestamp.current <= 800; likewise, make the mousedown handler no-op
(or not flip state) when within that same window so synthetic mousedown events
don't override the touch suppression. Ensure the ref is initialized to 0 and all
comparisons guard against 0 (no-touch) state.
🧹 Nitpick comments (4)
frontend/app/[locale]/quiz/[slug]/page.tsx (1)

17-17: Consider memoizing getQuizBySlug to avoid duplicate queries.

The getQuizBySlug function is called both here (line 17) and in QuizPage (line 46). If this is a direct database query rather than a fetch() call, Next.js won't automatically deduplicate it.

You could wrap the query function with React's cache() to ensure it's only executed once per request:

import { cache } from 'react';

const getCachedQuizBySlug = cache(getQuizBySlug);

This is a minor optimization since the query is likely fast, but it reduces database load.

frontend/components/q&a/QaSection.tsx (1)

31-37: Consider extracting shared clearSelection utility.

This function is duplicated in AccordionList.tsx. You could extract it to a shared utility (e.g., @/lib/utils or a local q&a/utils.ts) to reduce duplication.

frontend/app/[locale]/shop/products/[slug]/page.tsx (2)

42-49: Consider improving type safety for the merged product object.

The current approach using as any assertions loses all type information, which could lead to runtime errors if property names are mistyped or if the data structure changes.

While I understand this may be a pragmatic solution for merging data from two different sources (publicProduct from DB and resultProduct from page data), consider defining a proper merged type or using type guards.

♻️ Suggested approach
// Define a unified product type that covers both sources
type MergedProduct = typeof publicProduct & Partial<typeof resultProduct>;

const product: MergedProduct = {
  ...publicProduct,
  ...Object.fromEntries(
    Object.entries(resultProduct).filter(([, v]) => v !== undefined)
  ),
};

60-66: Remove unnecessary try/catch around translation lookup.

next-intl's t() function doesn't throw when a translation key is missing—it logs an error to the console and returns the key path by default. The try/catch block won't catch anything and adds unnecessary overhead.

If you need a custom fallback (returning the original badge instead of the key path), configure it via getMessageFallback() in your next-intl setup rather than wrapping the call.

🔧 Simplified approach
   const badgeLabel =
     badge && badge !== 'NONE'
-      ? (() => {
-          try {
-            return tProduct(`badges.${badge}`);
-          } catch {
-            return badge;
-          }
-        })()
+      ? tProduct(`badges.${badge}`)
       : null;

Then configure the fallback in your next-intl initialization:

getMessageFallback({namespace, key, error}) {
  return key; // or custom logic
}

</p>
</div>

<span className={outlineBtnStyles}>{t('supportLink')}</span>
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

The support link is rendered as a non-interactive span.

The supportLink translation key suggests this should be a clickable link, but it's rendered as a <span> with button styling and no interaction handlers. If this is intentional (e.g., a placeholder for future functionality or just a decorative badge), consider:

  • Adding aria-hidden="true" if purely decorative
  • Renaming the translation key to something like supportBadge

If it should be interactive, wrap it in a <Link> or <a> element with the appropriate destination.

🤖 Prompt for AI Agents
In `@frontend/app/`[locale]/dashboard/page.tsx at line 99, The support item is
rendered as a non-interactive <span> with the class outlineBtnStyles and
translation key supportLink; if it should be a clickable link, replace the
<span> with a proper interactive element (e.g., Next.js <Link> or an <a> with
the correct href) and preserve outlineBtnStyles and the t('supportLink') text,
or if it is purely decorative add aria-hidden="true" and/or rename the
translation key (supportBadge) to avoid implying interactivity; update the
element where t('supportLink') is used in page.tsx accordingly.

Comment on lines 54 to 57
<Link
href="/shop/products?filter=new"
href="/shop/products?filter=newest"
className="group border-border text-muted-foreground hover:text-foreground focus-visible:ring-offset-background inline-flex items-center gap-2 rounded-md border bg-transparent px-4 py-2 text-xs font-semibold tracking-[0.25em] uppercase shadow-none transition-[transform,box-shadow,color,filter] duration-500 ease-out hover:-translate-y-0.5 hover:shadow-[var(--shop-card-shadow-hover)] hover:brightness-110 focus-visible:ring-2 focus-visible:ring-[color:var(--color-ring)] focus-visible:ring-offset-2 focus-visible:outline-none sm:text-sm"
aria-label={t('viewAll')}
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "filter.*newest|newest.*filter" --type=tsx --type=ts -C 3

Repository: DevLoversTeam/devlovers.net

Length of output: 98


🏁 Script executed:

# Also check the products page to see how it handles filter parameters
fd "products" --type=f -e tsx -e ts | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 370


🏁 Script executed:

# Search for filter parameter handling in general
rg -n "filter=|filter:" --type=tsx --type=ts -C 2 | grep -A 5 -B 5 "shop/products"

Repository: DevLoversTeam/devlovers.net

Length of output: 98


🏁 Script executed:

rg -n "newest" --type=ts -C 3

Repository: DevLoversTeam/devlovers.net

Length of output: 4664


🏁 Script executed:

# Check the CatalogProductsClient to see how it handles filter parameters
cat -n frontend/components/shop/CatalogProductsClient.tsx | head -100

Repository: DevLoversTeam/devlovers.net

Length of output: 3520


🏁 Script executed:

# Check the products.ts query file
cat -n frontend/db/queries/shop/products.ts | head -100

Repository: DevLoversTeam/devlovers.net

Length of output: 3042


🏁 Script executed:

# Search for filter parameter handling more broadly
rg -n "filter" frontend/components/shop/ -C 2

Repository: DevLoversTeam/devlovers.net

Length of output: 3486


🏁 Script executed:

# Check the full catalog route handler to see if filter=newest is handled
cat -n frontend/app/api/shop/catalog/route.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 3350


🏁 Script executed:

# Check the catalogQuerySchema to see what parameters it accepts
rg -n "catalogQuerySchema" --type=ts -B 5 -A 15

Repository: DevLoversTeam/devlovers.net

Length of output: 9428


🏁 Script executed:

# Check the products page to see how it handles filter=newest
cat -n frontend/app/\[locale\]/shop/products/page.tsx | head -60

Repository: DevLoversTeam/devlovers.net

Length of output: 2293


🏁 Script executed:

# Verify what happens when filter parameter doesn't match any legacy case
rg -n "filter" frontend/app/\[locale\]/shop/products/page.tsx -B 2 -A 2

Repository: DevLoversTeam/devlovers.net

Length of output: 668


Use sort=newest instead of filter=newest

The link uses filter=newest, but the products page and catalog API only handle the legacy filter=new parameter. The modern parameter is sort=newest. Update the href to /shop/products?sort=newest to properly apply the newest sort when the "View All" link is clicked.

🤖 Prompt for AI Agents
In `@frontend/app/`[locale]/shop/page.tsx around lines 54 - 57, Update the Link
that renders the "View All" button so its query param uses the modern sort key:
change the href on the Link component (the JSX starting with <Link
href="/shop/products?filter=newest" ...>) to use /shop/products?sort=newest
instead of filter=newest so the products page and catalog API receive the
supported sort=newest parameter.

active = false;
};
}, [open, items.length, isLoading]);
}, [open, items.length, isLoading, locale]);
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

Locale change may not trigger data refresh if items are already cached.

Adding locale to the dependency array is a step in the right direction, but the effect's guard condition on line 77 (if (!open || items.length || !isLoading) return) prevents re-fetching when items.length is truthy. If a user loads search results, closes the dropdown, switches locale, and reopens, the stale items from the previous locale will be displayed.

Consider clearing items when locale changes:

🛠️ Suggested fix
+  useEffect(() => {
+    setItems([]);
+  }, [locale]);
+
   useEffect(() => {
     if (!open || items.length || !isLoading) return;
🤖 Prompt for AI Agents
In `@frontend/components/blog/BlogHeaderSearch.tsx` at line 98, The effect in
BlogHeaderSearch that depends on [open, items.length, isLoading, locale] is
still guarded by if (!open || items.length || !isLoading) return, so switching
locale leaves stale items in place; fix by clearing cached results when locale
changes—either add a separate useEffect that watches locale and calls
setItems([])/clearItems(), or modify the existing effect to treat a locale
change as invalidating items (e.g., check previous locale and call setItems([])
before the guard) so reopening after a locale switch triggers a fresh fetch.

return (
<div key={index} className="my-2 overflow-x-auto">
<table className="min-w-full border-collapse border border-gray-300 text-sm">
<table className="min-w-full border-collapse border border-gray-300 text-sm text-gray-900 dark:text-gray-900">
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

Dark mode text color may cause readability issues.

Using dark:text-gray-900 forces dark text in dark mode, which could result in poor contrast against dark backgrounds (e.g., the alternating bg-white / bg-gray-50 row colors don't have dark-mode variants). Consider using a lighter text color for dark mode or updating the row background colors as well.

Suggested fix
-      <table className="min-w-full border-collapse border border-gray-300 text-sm text-gray-900 dark:text-gray-900">
+      <table className="min-w-full border-collapse border border-gray-300 text-sm text-gray-900 dark:border-gray-600 dark:text-gray-100">

You may also need to update the row backgrounds at lines 249 and 262 to include dark mode variants.

🤖 Prompt for AI Agents
In `@frontend/components/q`&a/AccordionList.tsx at line 247, Change the table's
dark-mode text class so text remains readable in dark backgrounds by replacing
or removing "dark:text-gray-900" (e.g., use "dark:text-gray-100" or remove the
dark override) in the table element's className, and update the row background
classes "bg-white" and "bg-gray-50" on the table rows to include dark variants
(e.g., "dark:bg-gray-800" / "dark:bg-gray-700" or similar) so row contrast in
dark mode matches the new text color.

Comment on lines +24 to +32
const [remainingSeconds, setRemainingSeconds] = useState(timeLimitSeconds);
const [isSynced, setIsSynced] = useState(false);
const [prevEndTime, setPrevEndTime] = useState(endTime);

if (endTime !== prevEndTime) {
setPrevEndTime(endTime);
setIsSynced(false);
setRemainingSeconds(timeLimitSeconds);
}
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

🌐 Web query:

React function component setState during render allowed rules hooks

💡 Result:

  • Calling setState (e.g., a useState setter) during render is generally a bad idea because render should be pure (no side effects). React may render more than once, so side effects belong in event handlers or Effects (useEffect), not in the render body. [1]
  • Unconditionally setting state during render will cause an infinite re-render loop (React’s set-state-in-render lint exists specifically to catch this). [2]
  • React only “supports” render-phase updates for the same component, and even then only in carefully guarded cases. Updating another component’s state while rendering triggers a warning (“Cannot update a component… while rendering a different component…”). If you intentionally need to update another component as a result of rendering, React recommends wrapping that update in useEffect. [3]
  • Rules of Hooks are separate but related: you must call Hooks at the top level of a function component (or custom Hook), not inside conditions/loops/nested functions, so React sees them in the same order every render. [4]

Practical guidance: if you’re “setting state during render” to derive values from props/state, usually you should compute a derived value directly (no state), or synchronize state in useEffect when inputs change. [1][2][3]

Sources: React docs on purity [1], set-state-in-render lint [2], React 16.13 render-update warning note [3], Rules of Hooks [4].

[1] (es.react.dev)
[2] (react.dev)
[3] (legacy.reactjs.org)
[4] (react.dev)

Citations:


🏁 Script executed:

# Find and read the specific file mentioned in the review
fd -t f CountdownTimer.tsx

Repository: DevLoversTeam/devlovers.net

Length of output: 114


🏁 Script executed:

# Read the CountdownTimer.tsx file to verify the actual code
cat -n frontend/components/quiz/CountdownTimer.tsx | head -50

Repository: DevLoversTeam/devlovers.net

Length of output: 1654


🏁 Script executed:

# Let me check the broader context of how startedAt is passed and used
cat -n frontend/components/quiz/CountdownTimer.tsx | head -100

Repository: DevLoversTeam/devlovers.net

Length of output: 3530


Move state reset logic into an effect to avoid setState during render.

Calling setRemainingSeconds, setIsSynced, and setPrevEndTime in the render body violates React's purity rules. Since endTime is derived from props on each render, this can trigger infinite re-render loops if startedAt is recreated. Use useEffect with dependency array [endTime, timeLimitSeconds] and remove prevEndTime state.

Suggested fix
  const [remainingSeconds, setRemainingSeconds] = useState(timeLimitSeconds);
  const [isSynced, setIsSynced] = useState(false);
-  const [prevEndTime, setPrevEndTime] = useState(endTime);
-
-  if (endTime !== prevEndTime) {
-    setPrevEndTime(endTime);
-    setIsSynced(false);
-    setRemainingSeconds(timeLimitSeconds);
-  }

+  useEffect(() => {
+    setIsSynced(false);
+    setRemainingSeconds(timeLimitSeconds);
+  }, [endTime, timeLimitSeconds]);
📝 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
const [remainingSeconds, setRemainingSeconds] = useState(timeLimitSeconds);
const [isSynced, setIsSynced] = useState(false);
const [prevEndTime, setPrevEndTime] = useState(endTime);
if (endTime !== prevEndTime) {
setPrevEndTime(endTime);
setIsSynced(false);
setRemainingSeconds(timeLimitSeconds);
}
const [remainingSeconds, setRemainingSeconds] = useState(timeLimitSeconds);
const [isSynced, setIsSynced] = useState(false);
useEffect(() => {
setIsSynced(false);
setRemainingSeconds(timeLimitSeconds);
}, [endTime, timeLimitSeconds]);
🤖 Prompt for AI Agents
In `@frontend/components/quiz/CountdownTimer.tsx` around lines 24 - 32, The reset
logic that calls setRemainingSeconds, setIsSynced and setPrevEndTime during
render must be moved into a useEffect to avoid setState during render; remove
the prevEndTime state entirely and add a useEffect that watches [endTime,
timeLimitSeconds] and when endTime changes reset remainingSeconds (via
setRemainingSeconds(timeLimitSeconds)) and setIsSynced(false); ensure no state
updates occur in the render body and reference the existing state setters
remainingSeconds/setRemainingSeconds and isSynced/setIsSynced and the props
endTime and timeLimitSeconds inside that effect.

Comment on lines +37 to +48
let synced = false;

const interval = setInterval(() => {
const now = Date.now();
const remaining = Math.max(0, Math.floor((endTime - now) / 1000));

setRemainingSeconds(remaining);

if (!synced) {
synced = true;
setIsSynced(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

🧩 Analysis chain

🏁 Script executed:

cat -n frontend/components/quiz/CountdownTimer.tsx

Repository: DevLoversTeam/devlovers.net

Length of output: 5166


Use useRef to track sync state across visibility changes.

The local synced variable in the interval closure doesn't reset when the visibility handler calls setIsSynced(false), so the sync won't re-trigger. Replace the local flag with a useRef and reset it both when the effect starts and when visibility changes to properly re-sync after tab becomes visible.

🛠️ Suggested fix
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
@@
-  useEffect(() => {
+  const syncedRef = useRef(false);
+
+  useEffect(() => {
     if (!isActive) return;
 
-    let synced = false;
+    syncedRef.current = false;
+    setIsSynced(false);
@@
-      if (!synced) {
-        synced = true;
+      if (!syncedRef.current) {
+        syncedRef.current = true;
         setIsSynced(true);
       }
@@
     const handleVisibilityChange = () => {
       if (!document.hidden) {
+        syncedRef.current = false;
         setIsSynced(false);
         const remaining = Math.max(
           0,
           Math.floor((endTime - Date.now()) / 1000)
         );
🤖 Prompt for AI Agents
In `@frontend/components/quiz/CountdownTimer.tsx` around lines 37 - 48, The local
synced boolean in the interval closure (variable synced) doesn't reflect
visibility-driven resets; replace it with a React ref (e.g., syncedRef =
useRef(false)) and use syncedRef.current inside the setInterval callback instead
of the local variable, initialize/reset syncedRef.current = false at the start
of the effect, and also reset syncedRef.current = false inside the
visibility/change handler whenever you call setIsSynced(false) so the sync logic
(setIsSynced(true)) can run again after the tab becomes visible; ensure the
interval cleanup still clears and that you update any references to synced to
use the ref (syncedRef.current).

Comment on lines +34 to +77
useLayoutEffect(() => {
const calculatePosition = () => {
const mobile = window.innerWidth < 768;
let newTop = 0;

if (mobile && ctaRef.current) {
const rect = ctaRef.current.getBoundingClientRect();
const desired = rect.bottom + window.scrollY + rect.height + 14;
const popupHeight = 56;
const safeBottom = 16;
const max =
window.scrollY + window.innerHeight - popupHeight - safeBottom;
newTop = Math.min(desired, max);
}

setPosition({ top: newTop, isMobile: mobile });
};

calculatePosition();
}, [ctaRef]);
useEffect(() => {
const handleResize = () => {
const mobile = window.innerWidth < 768;
let newTop = 0;

if (mobile && ctaRef.current) {
const rect = ctaRef.current.getBoundingClientRect();
const desired = rect.bottom + window.scrollY + rect.height + 14;
const popupHeight = 56;
const safeBottom = 16;
const max =
window.scrollY + window.innerHeight - popupHeight - safeBottom;
newTop = Math.min(desired, max);
}

setPosition({ top: newTop, isMobile: mobile });
};

window.addEventListener('resize', handleResize);

return () => {
window.removeEventListener('resize', handleResize);
};
}, [ctaRef]);
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

Fix mobile top calculation for fixed positioning.

The computed top includes window.scrollY (and rect.height), but the container is position: fixed. This can push the popup off-screen after scrolling on mobile. Use viewport-relative values instead.

🛠️ Proposed fix
-      if (mobile && ctaRef.current) {
-        const rect = ctaRef.current.getBoundingClientRect();
-        const desired = rect.bottom + window.scrollY + rect.height + 14;
-        const popupHeight = 56;
-        const safeBottom = 16;
-        const max =
-          window.scrollY + window.innerHeight - popupHeight - safeBottom;
-        newTop = Math.min(desired, max);
-      }
+      if (mobile && ctaRef.current) {
+        const rect = ctaRef.current.getBoundingClientRect();
+        const desired = rect.bottom + 14;
+        const popupHeight = 56;
+        const safeBottom = 16;
+        const max = window.innerHeight - popupHeight - safeBottom;
+        newTop = Math.min(desired, max);
+      }
@@
-      if (mobile && ctaRef.current) {
-        const rect = ctaRef.current.getBoundingClientRect();
-        const desired = rect.bottom + window.scrollY + rect.height + 14;
-        const popupHeight = 56;
-        const safeBottom = 16;
-        const max =
-          window.scrollY + window.innerHeight - popupHeight - safeBottom;
-        newTop = Math.min(desired, max);
-      }
+      if (mobile && ctaRef.current) {
+        const rect = ctaRef.current.getBoundingClientRect();
+        const desired = rect.bottom + 14;
+        const popupHeight = 56;
+        const safeBottom = 16;
+        const max = window.innerHeight - popupHeight - safeBottom;
+        newTop = Math.min(desired, max);
+      }
📝 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
useLayoutEffect(() => {
const calculatePosition = () => {
const mobile = window.innerWidth < 768;
let newTop = 0;
if (mobile && ctaRef.current) {
const rect = ctaRef.current.getBoundingClientRect();
const desired = rect.bottom + window.scrollY + rect.height + 14;
const popupHeight = 56;
const safeBottom = 16;
const max =
window.scrollY + window.innerHeight - popupHeight - safeBottom;
newTop = Math.min(desired, max);
}
setPosition({ top: newTop, isMobile: mobile });
};
calculatePosition();
}, [ctaRef]);
useEffect(() => {
const handleResize = () => {
const mobile = window.innerWidth < 768;
let newTop = 0;
if (mobile && ctaRef.current) {
const rect = ctaRef.current.getBoundingClientRect();
const desired = rect.bottom + window.scrollY + rect.height + 14;
const popupHeight = 56;
const safeBottom = 16;
const max =
window.scrollY + window.innerHeight - popupHeight - safeBottom;
newTop = Math.min(desired, max);
}
setPosition({ top: newTop, isMobile: mobile });
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, [ctaRef]);
useLayoutEffect(() => {
const calculatePosition = () => {
const mobile = window.innerWidth < 768;
let newTop = 0;
if (mobile && ctaRef.current) {
const rect = ctaRef.current.getBoundingClientRect();
const desired = rect.bottom + 14;
const popupHeight = 56;
const safeBottom = 16;
const max = window.innerHeight - popupHeight - safeBottom;
newTop = Math.min(desired, max);
}
setPosition({ top: newTop, isMobile: mobile });
};
calculatePosition();
}, [ctaRef]);
useEffect(() => {
const handleResize = () => {
const mobile = window.innerWidth < 768;
let newTop = 0;
if (mobile && ctaRef.current) {
const rect = ctaRef.current.getBoundingClientRect();
const desired = rect.bottom + 14;
const popupHeight = 56;
const safeBottom = 16;
const max = window.innerHeight - popupHeight - safeBottom;
newTop = Math.min(desired, max);
}
setPosition({ top: newTop, isMobile: mobile });
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, [ctaRef]);
🤖 Prompt for AI Agents
In `@frontend/components/shared/OnlineCounterPopup.tsx` around lines 34 - 77, The
mobile top calculation in calculatePosition and handleResize incorrectly adds
window.scrollY (and duplicatively rect.height) even though the popup uses
position: fixed; update both functions to compute viewport-relative values: use
rect.bottom + 14 (do not add rect.height or window.scrollY) as the desired top,
compute max as window.innerHeight - popupHeight - safeBottom, then set newTop =
Math.min(desired, max); keep references to ctaRef and setPosition as-is and
apply the same change in both calculatePosition and handleResize.

Comment on lines +45 to +54
const stockRaw = (product as any).stock as unknown;
const stockNum =
typeof stockRaw === 'number'
? stockRaw
: typeof stockRaw === 'string' && stockRaw.trim().length > 0
? Number(stockRaw)
: NaN;

const maxQty =
Number.isFinite(stockNum) && stockNum > 0 ? Math.floor(stockNum) : null;
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:

#!/bin/bash
# First, find where ShopProduct is defined
echo "=== Looking for ShopProduct type definition ==="
rg -n "interface ShopProduct|type ShopProduct" frontend/lib/shop

echo ""
echo "=== Searching for stock field definition ==="
rg -n "stock.*:" frontend/lib/shop/data.ts

echo ""
echo "=== Check the actual file structure ==="
fd -t f -e ts -e tsx . frontend/lib/shop

Repository: DevLoversTeam/devlovers.net

Length of output: 632


🏁 Script executed:

#!/bin/bash
# Read the ShopProduct type definition from data.ts
echo "=== ShopProduct type definition ==="
cat -n frontend/lib/shop/data.ts | head -35

echo ""
echo "=== Check for ValidationShopProduct and stock field ==="
rg -A 15 "ValidationShopProduct" frontend/lib/shop/data.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 1728


🏁 Script executed:

#!/bin/bash
# Find and read the validation shop file
echo "=== Looking for validation/shop file ==="
fd -t f "shop" frontend/lib/validation

echo ""
echo "=== Read the validation shop file ==="
cat -n frontend/lib/validation/shop.ts | head -100

Repository: DevLoversTeam/devlovers.net

Length of output: 4197


🏁 Script executed:

#!/bin/bash
# Continue reading the file to find shopProductSchema
echo "=== Read shopProductSchema definition ==="
cat -n frontend/lib/validation/shop.ts | sed -n '90,200p'

Repository: DevLoversTeam/devlovers.net

Length of output: 4150


🏁 Script executed:

#!/bin/bash
# Read the AddToCartButton.tsx file around the problematic lines
echo "=== AddToCartButton.tsx lines 40-60 ==="
cat -n frontend/components/shop/AddToCartButton.tsx | sed -n '40,60p'

echo ""
echo "=== Check the component props/context to see where product comes from ==="
cat -n frontend/components/shop/AddToCartButton.tsx | sed -n '1,40p'

Repository: DevLoversTeam/devlovers.net

Length of output: 2306


Add stock field to ShopProduct type definition to eliminate unsafe type assertion.

The ShopProduct type (based on shopProductSchema) is missing the stock field that the component tries to access. Currently, the code requires (product as any).stock to bypass type checking. Update shopProductSchema in frontend/lib/validation/shop.ts to include stock as a field (similar to dbProductSchema which already defines it at line 111) to ensure proper type safety.

🤖 Prompt for AI Agents
In `@frontend/components/shop/AddToCartButton.tsx` around lines 45 - 54, The
ShopProduct type lacks the stock field causing a cast to any in
AddToCartButton.tsx; update shopProductSchema in frontend/lib/validation/shop.ts
to include the stock property (matching the shape in dbProductSchema, e.g.,
number or nullable/optional number as appropriate) so that the generated
ShopProduct type includes stock and you can remove the unsafe (product as
any).stock usage in AddToCartButton (which computes stockRaw/stockNum/maxQty).

const [isTabActive, setIsTabActive] = useState(true);
const [showWarning, setShowWarning] = useState(false);
const warningTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const lastInteractionWasTouch = useRef(false);
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:

fd -t f "useAntiCheat.ts" frontend/

Repository: DevLoversTeam/devlovers.net

Length of output: 101


🏁 Script executed:

cat -n frontend/hooks/useAntiCheat.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 4351


🌐 Web query:

synthetic mouse events after touchstart mobile browsers touch-to-mouse delay window

💡 Result:

On many mobile browsers, a single finger tap generates touch events first, then the browser may generate “compatibility” (synthetic) mouse events so older mouse-based code still works.

  • Typical sequence (single-touch tap): touchstart → (0+ touchmove) → touchendmousemovemousedownmouseupclick.[1]
  • Suppressing the synthetic mouse/click events: if you preventDefault() on touchstart/touchmove/touchend during the interaction, browsers generally won’t dispatch the follow-up mouse/click events.[1][2]
    • Chrome caveat: touchstart/touchmove listeners may be passive by default, so preventDefault() won’t work unless you register the listener with { passive: false }.[2]

About the “touch-to-mouse delay window” (the old 300ms)

Historically, many mobile browsers delayed the click (not touchstart) by ~300–350ms after touchend to detect double-tap-to-zoom.[3] This is now typically removed when the page is considered “mobile-optimized,” e.g. with:

  • <meta name="viewport" content="width=device-width">[3]
  • or (where supported) touch-action: manipulation/none, which also removes the 300ms click delay by disabling some default touch behaviors.[3][4]

Practical takeaway: don’t rely on exact timing; if you need one unified input model, prefer Pointer Events (plus touch-action) over mixing touch+mouse.[4]

Sources: MDN on touch+mouse event ordering/suppression[1], MDN on passive listeners affecting preventDefault()[2], Chrome “300ms tap delay” explanation and viewport opt-in[3], Pointer Events spec note on touch-action removing the 300ms delay[4].

[1] (devdoc.net)
[2] (developer.mozilla.org)
[3] (developer.chrome.com)
[4] (w3.org)

Citations:


Replace boolean touch flag with timestamp-based window to prevent stale state and synthetic event bypass.

The current boolean flag becomes stale—if a touch occurs but no subsequent mouse event fires, the flag remains true indefinitely, suppressing context-menu violations from legitimate later interactions (e.g., keyboard context menu). Additionally, on mobile browsers, synthetic mousedown events fire after touchstart/touchend, flipping the flag to false and reintroducing false positives when the synthetic contextmenu event follows. Use a timestamp instead: track the last touch time and only suppress context-menu violations within a short window (e.g., 800ms), ignoring mousedown events within that window.

Suggested fix (timestamp window)
-  const lastInteractionWasTouch = useRef(false);
+  const lastTouchAtRef = useRef<number>(0);

@@
     const handleContextMenu = (e: MouseEvent) => {
       e.preventDefault();
-      
-      if (!lastInteractionWasTouch.current) {
-        addViolation('context-menu');
-      }
+      const isRecentTouch = Date.now() - lastTouchAtRef.current < 800;
+      if (!isRecentTouch) addViolation('context-menu');
     };

@@
     const handleTouchStart = () => {
-      lastInteractionWasTouch.current = true;
+      lastTouchAtRef.current = Date.now();
     };

     const handleMouseDown = () => {
-      lastInteractionWasTouch.current = false;
+      // Ignore synthetic mouse events right after touch
+      if (Date.now() - lastTouchAtRef.current < 800) return;
+      lastTouchAtRef.current = 0;
     };
🤖 Prompt for AI Agents
In `@frontend/hooks/useAntiCheat.ts` at line 25, Replace the boolean ref
lastInteractionWasTouch with a timestamp ref (e.g., lastTouchTimestamp =
useRef<number>(0)) and update all touch/mouse/contextmenu handlers to use a
short suppression window (suggest ~800ms): set lastTouchTimestamp.current =
Date.now() on touchstart/touchend, and in the contextmenu handler
ignore/suppress violations if Date.now() - lastTouchTimestamp.current <= 800;
likewise, make the mousedown handler no-op (or not flip state) when within that
same window so synthetic mousedown events don't override the touch suppression.
Ensure the ref is initialized to 0 and all comparisons guard against 0
(no-touch) state.

@ViktorSvertoka ViktorSvertoka merged commit ee56430 into main Feb 5, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working performance Performance and efficiency optimizations without functional changes. refactor Code restructuring without functional changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants