Conversation
…(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
* (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
- 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
…=new and using sort=newest
(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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
✅ Deploy Preview for develop-devlovers ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughThis 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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 memoizinggetQuizBySlugto avoid duplicate queries.The
getQuizBySlugfunction is called both here (line 17) and inQuizPage(line 46). If this is a direct database query rather than afetch()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 sharedclearSelectionutility.This function is duplicated in
AccordionList.tsx. You could extract it to a shared utility (e.g.,@/lib/utilsor a localq&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 anyassertions 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 (
publicProductfrom DB andresultProductfrom 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'st()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
badgeinstead of the key path), configure it viagetMessageFallback()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> |
There was a problem hiding this comment.
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.
| <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')} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "filter.*newest|newest.*filter" --type=tsx --type=ts -C 3Repository: 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 -20Repository: 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 3Repository: 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 -100Repository: DevLoversTeam/devlovers.net
Length of output: 3520
🏁 Script executed:
# Check the products.ts query file
cat -n frontend/db/queries/shop/products.ts | head -100Repository: DevLoversTeam/devlovers.net
Length of output: 3042
🏁 Script executed:
# Search for filter parameter handling more broadly
rg -n "filter" frontend/components/shop/ -C 2Repository: 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.tsRepository: 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 15Repository: 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 -60Repository: 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 2Repository: 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]); |
There was a problem hiding this comment.
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"> |
There was a problem hiding this comment.
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.
| const [remainingSeconds, setRemainingSeconds] = useState(timeLimitSeconds); | ||
| const [isSynced, setIsSynced] = useState(false); | ||
| const [prevEndTime, setPrevEndTime] = useState(endTime); | ||
|
|
||
| if (endTime !== prevEndTime) { | ||
| setPrevEndTime(endTime); | ||
| setIsSynced(false); | ||
| setRemainingSeconds(timeLimitSeconds); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
React function component setState during render allowed rules hooks
💡 Result:
- Calling
setState(e.g., auseStatesetter) 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-renderlint 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:
- 1: https://es.react.dev/reference/rules/components-and-hooks-must-be-pure?utm_source=openai
- 2: https://react.dev/reference/eslint-plugin-react-hooks/lints/set-state-in-render?utm_source=openai
- 3: https://legacy.reactjs.org/blog/2020/02/26/react-v16.13.0.html?utm_source=openai
- 4: https://react.dev/reference/rules/rules-of-hooks?utm_source=openai
🏁 Script executed:
# Find and read the specific file mentioned in the review
fd -t f CountdownTimer.tsxRepository: 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 -50Repository: 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 -100Repository: 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.
| 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.
| 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); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n frontend/components/quiz/CountdownTimer.tsxRepository: 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).
| 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]); |
There was a problem hiding this comment.
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.
| 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.
| 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; |
There was a problem hiding this comment.
🧩 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/shopRepository: 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.tsRepository: 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 -100Repository: 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); |
There was a problem hiding this comment.
🧩 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.tsRepository: 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) →touchend→mousemove→mousedown→mouseup→click.[1] - Suppressing the synthetic mouse/click events: if you
preventDefault()ontouchstart/touchmove/touchendduring the interaction, browsers generally won’t dispatch the follow-up mouse/click events.[1][2]- Chrome caveat:
touchstart/touchmovelisteners may be passive by default, sopreventDefault()won’t work unless you register the listener with{ passive: false }.[2]
- Chrome caveat:
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:
- 1: https://devdoc.net/web/developer.mozilla.org/en-US/docs/Web/API/Touch_events/Supporting_both_TouchEvent_and_MouseEvent.html?utm_source=openai
- 2: https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent?utm_source=openai
- 3: https://developer.chrome.com/blog/300ms-tap-delay-gone-away?utm_source=openai
- 4: https://www.w3.org/TR/pointerevents2/?utm_source=openai
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.
Release v0.5.4 - performance, SEO, UX and accessibility improvements
Summary by CodeRabbit
New Features
Bug Fixes
Improvements