(SP: 2) [App] Upgrade Next.js dependencies and format codebase#355
(SP: 2) [App] Upgrade Next.js dependencies and format codebase#355ViktorSvertoka merged 1 commit intodevelopfrom
Conversation
|
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 is a large-scale code styling and formatting overhaul across the entire frontend codebase, including import statement restructuring, JSX indentation adjustments, Tailwind CSS class reordering, JSON snapshot formatting, and quote style standardization. Minimal logic changes; primarily cosmetic refactoring to improve consistency and readability. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 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: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
frontend/lib/psp/monobank.ts (1)
874-893:⚠️ Potential issue | 🔴 CriticalConcurrent force-refresh stampede risk on key rotation — verified.
If multiple webhook requests arrive simultaneously and all fail initial signature verification (e.g., after a key rotation), each independently enters the force-refresh block and calls
fetchWebhookPubKey({ forceRefresh: true }). The module-level cache is bypassed on force-refresh, and there is no in-flight deduplication, so N concurrent failing requests generate N parallel network calls to the Monobank pubkey endpoint. This can trigger rate limiting or amplify cascading failures.Implement module-level in-flight promise deduplication to collapse concurrent force-refresh calls:
🔒 Proposed fix — deduplicate concurrent force-refresh calls
+let _pubkeyRefreshInFlight: Promise<Uint8Array> | null = null; + +async function fetchWebhookPubKeyWithDedup(): Promise<Uint8Array> { + if (_pubkeyRefreshInFlight) return _pubkeyRefreshInFlight; + _pubkeyRefreshInFlight = fetchWebhookPubKey({ forceRefresh: true }).finally( + () => { _pubkeyRefreshInFlight = null; } + ); + return _pubkeyRefreshInFlight; +} try { - const refreshed = await fetchWebhookPubKey({ forceRefresh: true }); + const refreshed = await fetchWebhookPubKeyWithDedup(); const ok = verifyWebhookSignature(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/lib/psp/monobank.ts` around lines 874 - 893, The issue: concurrent webhook requests that fail verification each call fetchWebhookPubKey({ forceRefresh: true }) and cause a stampede; fix by adding module-level in-flight deduplication for force-refresh calls so only one network fetch runs at a time. Implement a shared variable (e.g., inFlightFetchWebhookPubKeyPromise) in the same module and update fetchWebhookPubKey to return the existing promise when a forceRefresh request is already pending; ensure the promise is cleared on both success and error so future refreshes retry, and keep existing behavior for non-forceRefresh calls and for verifyWebhookSignature/monoLogInfo/monoLogError use unchanged.frontend/db/queries/users.ts (1)
48-65:⚠️ Potential issue | 🟡 MinorRaw SQL bypasses Drizzle schema; type assertion with
any[]discards type safety.Two issues visible in this function:
Hardcoded names in raw SQL —
point_transactions,user_id, andpointsare literal strings, not derived from the importedpointTransactionsschema object. A table or column rename via migration won't be caught at compile time. Compare togetUserProfile, which uses${pointTransactions.points}andpointTransactions.userIdto bind schema references.Unsafe type assertion —
result as { rows: any[] }loses type information. The.rowsproperty is valid for the PostgreSQL adapters in use (node-postgres and neon-http), but usingany[]discards row typing. Replace with a type guard or properly typed assertion (e.g.,as { rows: Array<{ rank: number }> }) to preserve type safety.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/db/queries/users.ts` around lines 48 - 65, Replace the raw SQL text in rankQuery with schema-bound identifiers from the imported pointTransactions object (use `${pointTransactions}`, `${pointTransactions.userId}`, `${pointTransactions.points}` or their appropriate column symbols) so table/column names are tied to the Drizzle schema, and update the db.execute result typing by replacing `result as { rows: any[] }` with a properly typed assertion or type guard (e.g., `as { rows: Array<{ rank: number }> }`) so rankRow preserves its shape; reference the existing symbols rankQuery, pointTransactions, db.execute, and rankRow when making these changes.
🧹 Nitpick comments (31)
frontend/lib/auth/password-bytes.ts (1)
1-3: Consider hoistingTextEncoderto module scope.A new
TextEncoderinstance is allocated on every call. Moving it to a module-level constant is a zero-risk micro-optimisation that avoids repeated allocations, especially relevant if this is called during form validation on every keystroke.♻️ Proposed refactor
+const encoder = new TextEncoder(); + export function utf8ByteLength(value: string): number { - return new TextEncoder().encode(value).length; + return encoder.encode(value).length; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/lib/auth/password-bytes.ts` around lines 1 - 3, Hoist the TextEncoder instance to module scope and reuse it inside utf8ByteLength to avoid allocating a new encoder on every call; create a module-level constant (e.g., const textEncoder = new TextEncoder()) and change utf8ByteLength(value: string) to call textEncoder.encode(value).length so the function uses the shared encoder instead of instantiating one each invocation.frontend/components/header/MobileMenuContext.tsx (1)
76-76: Hardcoded locale list in regex may silently break the same-page guard.
pathname.replace(/^\/(en|uk|pl)/, '')hard-codes supported locales. If a new locale is added to the i18n config without updating this regex,strippedPathnameretains the prefix and the equality check on line 78 always fails, causing a redundantrouter.pushfor same-page links instead of just closing the menu.Consider deriving the supported locales from a single source of truth (e.g., the i18n config) or using a generic locale-segment regex:
♻️ Suggested approach
-const strippedPathname = pathname.replace(/^\/(en|uk|pl)/, '') || '/'; +// Import `locales` array from your i18n config so this stays in sync automatically +import { locales } from '@/i18n/config'; +const localePattern = new RegExp(`^\\/(${locales.join('|')})`); +const strippedPathname = pathname.replace(localePattern, '') || '/';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/header/MobileMenuContext.tsx` at line 76, The runtime bug is caused by hardcoding the locale list in the regex used to compute strippedPathname (const strippedPathname = pathname.replace(/^\/(en|uk|pl)/, '') || '/'), which will keep a new locale prefix and break the same-page equality check; update the logic in MobileMenuContext to derive supported locales from the app's i18n config (or use a generic locale-segment regex like /^\/[a-z]{2}(-[A-Z]{2})?/) and then strip that dynamic prefix from pathname when computing strippedPathname so the equality check that prevents router.push works for all configured locales (modify the code around strippedPathname and where same-page navigation is checked/handled).frontend/components/auth/ResetPasswordForm.tsx (1)
21-23: Remove the localutf8ByteLength— it duplicates the shared lib export.
frontend/lib/auth/password-bytes.tsalready exports an identicalutf8ByteLength. The local redefinition diverges silently if the canonical implementation ever changes.♻️ Proposed fix
Remove the local definition and add an import:
import { Button } from '@/components/ui/button'; import { PASSWORD_MAX_BYTES, PASSWORD_MIN_LEN, PASSWORD_POLICY_REGEX, } from '@/lib/auth/signup-constraints'; +import { utf8ByteLength } from '@/lib/auth/password-bytes'; type ResetPasswordFormProps = { token: string; }; -function utf8ByteLength(value: string): number { - return new TextEncoder().encode(value).length; -} -🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/auth/ResetPasswordForm.tsx` around lines 21 - 23, Remove the local utf8ByteLength function in ResetPasswordForm.tsx and import the shared implementation instead; replace the local definition with an import of utf8ByteLength from the shared password-bytes module and ensure all uses in ResetPasswordForm reference the imported utf8ByteLength symbol.frontend/lib/shop/status-token.ts (1)
106-107: Bracelessifis inconsistent with the braced style used immediately below.Line 108 uses a braced block for a similar guard. Keeping the style consistent reduces future "dangling else" / accidental multi-statement risks.
♻️ Suggested consistency fix
- if (!payload || payload.v !== 1) - return { ok: false, reason: 'invalid_payload' }; + if (!payload || payload.v !== 1) { + return { ok: false, reason: 'invalid_payload' }; + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/lib/shop/status-token.ts` around lines 106 - 107, The single-line guard "if (!payload || payload.v !== 1) return { ok: false, reason: 'invalid_payload' };" should be converted to a braced block to match the braced style used immediately below: replace the one-line if with a block form (e.g., if (!payload || payload.v !== 1) { return { ok: false, reason: 'invalid_payload' }; }) so the guard around the payload/v check uses braces and reduces accidental multi-statement errors; locate the check around the payload.v verification in status-token.ts and update that if statement accordingly.frontend/lib/admin/tiptap-transforms.ts (1)
32-35: Consider adding braces to the multi-lineifbodies for consistency and safety.Lines 32–35 drop curly braces while using a two-line body, yet the immediately adjacent line 36 uses an inline one-liner, creating a mixed style within the same block. Brace-less multi-line
ifbodies are the most common source of "off-by-indentation" bugs when either branch is later extended.♻️ Suggested fix
- if (child.type === 'bulletList') - return listBlockToTipTap(child, 'bulletList'); - if (child.type === 'numberedList') - return listBlockToTipTap(child, 'orderedList'); - if (child.type === 'code') return codeBlockToTipTap(child); + if (child.type === 'bulletList') return listBlockToTipTap(child, 'bulletList'); + if (child.type === 'numberedList') return listBlockToTipTap(child, 'orderedList'); + if (child.type === 'code') return codeBlockToTipTap(child);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/lib/admin/tiptap-transforms.ts` around lines 32 - 35, The two multi-line if branches checking child.type ('bulletList' and 'numberedList') call listBlockToTipTap but omit braces; add curly braces around each of those if bodies so they match the surrounding style and avoid future indentation bugs—specifically update the branches that invoke listBlockToTipTap(child, 'bulletList') and listBlockToTipTap(child, 'orderedList') to use { ... } blocks within the same function that processes child nodes.frontend/drizzle/meta/0006_snapshot.json (1)
1-2694: Consider excluding machine-generated Drizzle snapshots from the formatter.All changes here are correctly formatted and semantically identical to the previous version — no data, FK mappings, or constraints were altered. However, Drizzle Kit regenerates
drizzle/meta/*.jsonon everydrizzle-kit generateordrizzle-kit push, using its own formatting style. Running Prettier over these files will cause recurring diff churn each time the migration toolchain runs. The standard practice is to addfrontend/drizzle/meta/(and similar generated paths likedrizzle/migrations/) to.prettierignore.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/drizzle/meta/0006_snapshot.json` around lines 1 - 2694, The Drizzle-generated snapshot JSONs (e.g., the file with id "a2bbe8f7-7211-4508-ac7b-5a1f495d31cc" under frontend/drizzle/meta/) are machine-regenerated and cause pointless Prettier churn; update the project's .prettierignore to exclude the generated Drizzle metadata and migrations by adding entries for frontend/drizzle/meta/ and the Drizzle migrations folder (e.g., drizzle/migrations/) so files like frontend/drizzle/meta/0006_snapshot.json are not formatted by Prettier.frontend/lib/env/monobank.ts (2)
21-21:MonobankConfigtype is declared after it is used.
getMonobankConfigreferencesMonobankConfigat line 21 while the type declaration appears at line 41. TypeScript hoists type declarations so this compiles cleanly, but the conventional style is to declare types before their first use. Consider moving theMonobankConfigtype abovegetMonobankConfig.Also applies to: 41-51
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/lib/env/monobank.ts` at line 21, Move the MonobankConfig type declaration so it appears before its first use in getMonobankConfig; specifically, place the MonobankConfig type (and any other related types currently declared in the 41-51 region) above the getMonobankConfig function so the type definitions precede references to them.
59-69:parseTimeoutMsandparsePositiveIntare functionally identical — deduplicate.Both functions have the exact same implementation.
parseTimeoutMsshould either be removed and replaced withparsePositiveInt, or both should be unified under a single name that's semantically neutral (e.g.,parsePosInt).♻️ Proposed refactor
-function parseTimeoutMs(raw: string | undefined, fallback: number): number { - const v = raw ? Number.parseInt(raw, 10) : NaN; - if (!Number.isFinite(v) || v <= 0) return fallback; - return v; -} - function parsePositiveInt(raw: string | undefined, fallback: number): number { const v = raw ? Number.parseInt(raw, 10) : NaN; if (!Number.isFinite(v) || v <= 0) return fallback; return v; }Then update the call site in
getMonobankEnv:- const invoiceTimeoutMs = parseTimeoutMs( + const invoiceTimeoutMs = parsePositiveInt( env.MONO_INVOICE_TIMEOUT_MS, runtimeEnv.NODE_ENV === 'production' ? 8000 : 12000 );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/lib/env/monobank.ts` around lines 59 - 69, The two identical functions parseTimeoutMs and parsePositiveInt should be deduplicated: remove one (e.g., delete parseTimeoutMs) and keep a single semantically neutral function (e.g., parsePositiveInt or parsePosInt) with the same implementation, then update all call sites (notably getMonobankEnv) to call the retained function name; ensure exports/usage are updated accordingly so no references to the removed name remain.frontend/lib/psp/monobank.ts (2)
881-885:monoLogInfofires even when signature is still invalid after refresh.The
invalid_signaturecase is a security-relevant event (a request with an unverifiable signature even after key rotation) and arguably warrantsmonoLogWarnrather thanmonoLogInfo.♻️ Suggested refactor — split log by result
- monoLogInfo(MONO_PUBKEY_REFRESHED, { - endpoint: '/api/merchant/pubkey', - reason: 'refresh_once_after_verify_failed', - status: ok ? 'verified' : 'invalid_signature', - }); + if (ok) { + monoLogInfo(MONO_PUBKEY_REFRESHED, { + endpoint: '/api/merchant/pubkey', + reason: 'refresh_once_after_verify_failed', + status: 'verified', + }); + } else { + monoLogWarn(MONO_SIG_INVALID, { + endpoint: '/api/merchant/pubkey', + reason: 'invalid_signature_after_refresh', + }); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/lib/psp/monobank.ts` around lines 881 - 885, The log currently always calls monoLogInfo with event MONO_PUBKEY_REFRESHED and sets status to ok ? 'verified' : 'invalid_signature'; change this to split the outcome so that when ok is true you call monoLogInfo(MONO_PUBKEY_REFRESHED, { endpoint: '/api/merchant/pubkey', reason: 'refresh_once_after_verify_failed', status: 'verified' }) but when ok is false call monoLogWarn(MONO_PUBKEY_REFRESHED, { endpoint: '/api/merchant/pubkey', reason: 'refresh_once_after_verify_failed', status: 'invalid_signature' }) so unverifiable-signature cases are logged as warnings rather than info.
483-503: Module-level pubkey cache is instance-local — each serverless cold start will re-fetch.
_cachedWebhookKeyis process-local. In Next.js serverless deployments (or any environment with ephemeral function instances), every cold start discards the cache and triggers a pubkey network call on the first webhook. Under high concurrency with frequent cold starts this can amplify calls to the Monobank pubkey endpoint.Consider externalising the cache (e.g., Redis with a TTL matching
PUBKEY_TTL_MS) if the deployment runs at significant scale, or at minimum document this behaviour for operators.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/lib/psp/monobank.ts` around lines 483 - 503, The module-level variable _cachedWebhookKey used by getCachedWebhookKey() and cacheWebhookKey() is process-local and will be lost on serverless cold starts causing repeated pubkey fetches; modify the implementation to externalize the cache (e.g., use Redis or another shared store) keyed by a stable name and set with a TTL matching PUBKEY_TTL_MS, update getCachedWebhookKey() to read from the shared store and cacheWebhookKey() to write the key+expiry into that store, and if you cannot introduce a shared cache now, add clear documentation near PUBKEY_TTL_MS and these functions explaining the process-local behavior and recommending externalization for high-scale serverless deployments.frontend/components/q&a/Pagination.tsx (1)
203-203: Consider replacingoutline-nonewithoutline-hiddenfor Tailwind v4 compatibility.In Tailwind v4,
outline-nonewas repurposed to setoutline-style: none(removing the outline entirely), whileoutline-hiddenprovides the old v3 behavior—an invisible outline that remains visible in Windows High Contrast Mode / forced-colors. Since this line is being touched and the component already includesfocus:bg-[var(--qa-accent-soft)]for sighted users, switching tooutline-hiddenensures focus remains perceivable in forced-colors mode without breaking changes.✏️ Suggested change to
outline-hidden- className="min-w-20 appearance-none bg-transparent px-3 py-2 pr-9 text-sm font-medium text-gray-800 transition-colors outline-none hover:bg-[var(--qa-accent-soft)] focus:bg-[var(--qa-accent-soft)] dark:text-gray-200" + className="min-w-20 appearance-none bg-transparent px-3 py-2 pr-9 text-sm font-medium text-gray-800 transition-colors outline-hidden hover:bg-[var(--qa-accent-soft)] focus:bg-[var(--qa-accent-soft)] dark:text-gray-200"Note:
outline-noneis used in 12+ locations across the codebase (frontend/lib/,frontend/components/). A broader sweep may be considered, but this pattern is common in Tailwind v4 projects with custom focus styling in place.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/q`&a/Pagination.tsx at line 203, The className in the Pagination component uses Tailwind's deprecated outline-none which removes focus outlines in forced-colors modes; change the utility to outline-hidden in the className string inside the Pagination component (the class that currently includes "outline-none" and "focus:bg-[var(--qa-accent-soft)]") so focus remains perceivable in Windows High Contrast / forced-colors mode while keeping the existing custom focus background behavior.frontend/db/queries/users.ts (1)
67-68: Consider a nullish check instead of falsy forrankRow.rank.
!rankRow.ranktreats0as absent. SQLRANK()always yields ≥ 1 so it's safe today, but a nullish guard is semantically clearer and more defensive if the query ever changes.♻️ Proposed refactor
- if (!rankRow || !rankRow.rank) { + if (!rankRow || rankRow.rank == null) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/db/queries/users.ts` around lines 67 - 68, The current guard treats a zero rank as absent; update the condition that checks rankRow and rankRow.rank so it only returns null when rankRow is missing or rankRow.rank is null/undefined (i.e., use a nullish check for rankRow.rank rather than a falsy check). Locate the check using the symbol rankRow and replace the falsy test on rankRow.rank with an explicit null/undefined check (you can use optional chaining or an equality comparison to null) so a numeric 0 would be preserved as a valid rank.frontend/app/[locale]/shop/products/[slug]/page.tsx (2)
27-28: Parallelize independent translation fetches to reduce TTFB.The three
awaitcalls (getTranslations× 2 andgetMessages) are fully independent and execute serially today, adding their latencies back-to-back on every SSR request.♻️ Proposed refactor — parallel awaits
- const t = await getTranslations('shop.products'); - const tProduct = await getTranslations('shop.product'); + const [t, tProduct, messages] = await Promise.all([ + getTranslations('shop.products'), + getTranslations('shop.product'), + getMessages(), + ]); const currency = resolveCurrencyFromLocale(locale); // ... - const messages = await getMessages();Also applies to: 57-57
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/`[locale]/shop/products/[slug]/page.tsx around lines 27 - 28, The three independent awaits (getTranslations('shop.products') assigned to t, getTranslations('shop.product') assigned to tProduct, and getMessages(...) assigned to messages) are executed serially; change them to run in parallel using Promise.all so they resolve concurrently (e.g., call Promise.all on the three promise-returning calls and destructure the results back into t, tProduct, messages). Apply the same pattern where similar independent awaits occur later (referenced at the block around line 57) to reduce SSR TTFB.
15-18: Replace staticmetadatawithgenerateMetadatafor per-product SEO.Every product slug currently renders with the same generic title and description. This defeats the purpose of a product detail page from an SEO standpoint.
♻️ Proposed refactor — dynamic metadata
-export const metadata: Metadata = { - title: 'Product name | DevLovers', - description: 'Details, price, and availability for product.', -}; export const dynamic = 'force-dynamic'; +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string; locale: string }>; +}): Promise<Metadata> { + const { slug, locale } = await params; + const currency = resolveCurrencyFromLocale(locale); + const product = await getPublicProductBySlug(slug, currency); + if (!product) return {}; + return { + title: `${(product as any).name} | DevLovers`, + description: (product as any).description ?? 'Details, price, and availability for product.', + }; +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/`[locale]/shop/products/[slug]/page.tsx around lines 15 - 18, The static export const metadata should be replaced with an async export async function generateMetadata({ params }): Promise<Metadata> that derives title and description per product slug; inside generateMetadata use params.slug to fetch the product (call your existing product-fetch helper or API), return { title: `${product.name} | DevLovers`, description: product.description ?? 'Details, price, and availability for product.' }, and provide a sensible fallback title/description if the product fetch fails or product is not found.frontend/components/home/InteractiveCTAButton.tsx (1)
3-9: Consider migrating fromframer-motionto the canonicalmotion/reactimport path.There are no breaking changes in Motion for React in version 12, but the recommended upgrade path is to uninstall
framer-motionand installmotion. All APIs used here (AnimatePresence,motion,useMotionTemplate,useMotionValue,useSpring) are available at the same names undermotion/react.♻️ Proposed import migration
-import { - AnimatePresence, - motion, - useMotionTemplate, - useMotionValue, - useSpring, -} from 'framer-motion'; +import { + AnimatePresence, + motion, + useMotionTemplate, + useMotionValue, + useSpring, +} from 'motion/react';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/home/InteractiveCTAButton.tsx` around lines 3 - 9, The import currently pulls AnimatePresence, motion, useMotionTemplate, useMotionValue, and useSpring from 'framer-motion'; update the module import to 'motion/react' so these symbols are imported from the canonical package (replace the import statement that references AnimatePresence, motion, useMotionTemplate, useMotionValue, useSpring). If you haven't already, uninstall the old framer-motion package and install the new motion package so the new import resolves correctly.frontend/components/home/WelcomeHeroSection.tsx (1)
3-3: Migrate imports tomotion/reactconsistently across all affected files.The project imports
framer-motionin at least 19 files. Given that framer-motion v12 is installed and this PR focuses on dependency upgrades, now is the right time to align with the official migration path. Motion's upgrade guide confirms no breaking changes in v12—this is a straightforward package and import path swap.This requires two steps:
- Uninstall
framer-motionand install themotionpackage- Update all imports from
framer-motiontomotion/reactAffected files:
frontend/components/home/WelcomeHeroSection.tsxfrontend/components/shared/DynamicGridBackground.tsxfrontend/components/theme/ThemeToggle.tsxfrontend/components/home/FlipCardQA.tsxfrontend/components/home/InteractiveCTAButton.tsxfrontend/components/home/FloatingCode.tsxfrontend/components/leaderboard/LeaderboardClient.tsxfrontend/components/dashboard/AchievementsSection.tsxfrontend/components/dashboard/StatsCard.tsxfrontend/components/dashboard/ProfileCard.tsxfrontend/components/dashboard/ActivityHeatmapCard.tsxfrontend/components/dashboard/AchievementBadge.tsxfrontend/components/about/TopicsSection.tsxfrontend/components/about/SponsorsWall.tsxfrontend/components/about/PricingSection.tsxfrontend/components/about/HeroSection.tsxfrontend/components/about/InteractiveGame.tsxfrontend/components/about/FeaturesSection.tsxfrontend/components/leaderboard/LeaderboardPodium.tsxAll imports use the same API surface (
motion,AnimatePresence,useMotionTemplate,useMotionValue,useReducedMotion) and are directly compatible withmotion/react.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/home/WelcomeHeroSection.tsx` at line 3, Update the Framer Motion imports to use the new package and path: replace imports from 'framer-motion' with 'motion/react' for symbols like motion, AnimatePresence, useMotionTemplate, useMotionValue, and useReducedMotion (e.g., in WelcomeHeroSection.tsx and the other listed components), and ensure the project dependency is switched from framer-motion to the motion package (uninstall framer-motion and install motion) so all components import from 'motion/react' consistently.frontend/components/admin/quiz/AnswerEditor.tsx (1)
48-48: Preferfocus:outline-hiddenoverfocus:outline-nonein Tailwind v4.In Tailwind v4,
outline-noneonly removes theoutline-styleproperty. As per the Tailwind v4 docs: "outline-none now only removes outline style. Useoutline-hiddento remove the outline completely." The intended effect here is to suppress the browser's default focus outline while the customfocus:ring-*provides the visible indicator.focus:outline-hiddenis the correct Tailwind v4 utility for this purpose.♻️ Proposed fix
- className="border-border bg-background text-foreground placeholder:text-muted-foreground flex-1 rounded-md border px-3 py-1.5 text-sm focus:ring-1 focus:ring-[var(--accent-primary)] focus:outline-none" + className="border-border bg-background text-foreground placeholder:text-muted-foreground flex-1 rounded-md border px-3 py-1.5 text-sm focus:ring-1 focus:ring-[var(--accent-primary)] focus:outline-hidden"As per coding guidelines, the Tailwind v4 library documentation specifies: "outline-none now only removes outline style. Use outline-hidden to remove the outline completely."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/admin/quiz/AnswerEditor.tsx` at line 48, The Tailwind class on the input in AnswerEditor.tsx uses focus:outline-none which in Tailwind v4 only removes outline-style; replace it with focus:outline-hidden so the browser focus outline is fully suppressed while keeping the custom focus:ring (i.e., update the className string containing "focus:ring-1 focus:ring-[var(--accent-primary)] focus:outline-none" to use "focus:outline-hidden" instead).frontend/components/home/InteractiveConstellation.tsx (1)
146-347:iconsobject recreated every render but used insideuseEffect— pre-existing, low priority.The
iconsrecord (Line 40) is declared in the component body and captured by theuseEffectclosure, but it's not in the dependency array. Sinceiconshas no reactive dependencies this works correctly, but if this component is ever refactored, consider movingiconsinside the effect or extracting it to module scope to avoid the stale-closure footgun.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/home/InteractiveConstellation.tsx` around lines 146 - 347, The icons object is recreated every render and captured by the useEffect closure in InteractiveConstellation, which can cause stale-closure bugs; fix by either moving the icons declaration into the useEffect where draw/initParticles use it, or extract icons to module scope (so it’s stable across renders), or alternatively include icons in the useEffect dependency array; update references to icons inside initParticles/draw accordingly to ensure the effect always sees the correct, stable icons value.frontend/components/auth/fields/EmailField.tsx (1)
28-43: Pre-existing: hardcoded English strings alongside translated messages.Lines 30 and 41 use hardcoded English strings for
tooShort/tooLongvalidation, while Lines 24 and 36 uset('validation.required')andt('validation.invalidEmail')fromnext-intl. Not introduced by this PR, but worth a follow-up to align all validation messages with the i18n system.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/auth/fields/EmailField.tsx` around lines 28 - 43, The EmailField component currently sets hardcoded English messages for length validation; replace those with i18n calls so all validation uses next-intl: change the tooShort branch to input.setCustomValidity(t('validation.minLength', { min: minLength })) and the tooLong branch to input.setCustomValidity(t('validation.maxLength', { max: maxLength })), ensuring the component imports/has access to the t function already used for t('validation.required') and t('validation.invalidEmail'), and keep existing early returns and guards (minLength/maxLength) intact so behavior doesn't change.frontend/lib/github-stars.ts (1)
62-64: Optional: use project-level structured logging instead ofconsole.warn/console.error.Other server-side files (e.g., the admin quiz routes) use
logWarn/logErrorfrom@/lib/logging. Replacing the rawconsolecalls in this file would improve log consistency and ensure any structured-log enrichment (request-id, env, etc.) is applied uniformly.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/lib/github-stars.ts` around lines 62 - 64, Replace the raw console.warn/console.error calls in this file with the project-level structured logger (import logWarn and logError from "@/lib/logging"), update the existing console.warn(`⚠️ GitHub stargazers API error [${url}]: ${res.status} ${res.statusText}`) to call logWarn with the same message and any relevant metadata (e.g., { url, status: res.status, statusText: res.statusText }), and add imports at the top of the file; if there are other console calls in functions like the GitHub stargazers fetcher, convert them similarly to use logWarn/logError for consistent structured logging.frontend/app/api/admin/quiz/[id]/questions/route.ts (1)
166-176: Pre-existing: hardcoded assumption of exactly 4 answers per question.Not introduced by this PR, but worth noting:
Math.floor(i / 4)andi % 4on lines 168–169 assume every question has exactly 4 answers. If any question has a different answer count, the translation mapping will be incorrect. Consider computing the index dynamically based on actual answer counts if the schema doesn't enforce exactly 4.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/api/admin/quiz/`[id]/questions/route.ts around lines 166 - 176, The mapping for inserting quizAnswerTranslations assumes exactly 4 answers per question by using Math.floor(i / 4) and i % 4, which breaks if answer counts vary; update the logic in the block that inserts into quizAnswerTranslations (using insertedAnswers, questions, LOCALES) to compute qIdx and aIdx from the actual structure—e.g., iterate questions and their answers to produce translated rows or maintain a running counter of answersPerQuestion so quizAnswerId is paired with the correct question and answer index rather than using a hardcoded 4; ensure the produced objects still include quizAnswerId, locale, and answerText from questions[qIdx].answers[aIdx][locale].frontend/db/queries/quizzes/quiz.ts (1)
25-29: Add a blank line before theexport typeblock.There is no blank line between the last
importstatement (ending at line 24) and theexport typere-export, making it visually indistinguishable from the import list.🔧 Suggested fix
} from '../../schema/quiz'; + export type { QuizAnswer, QuizQuestion, QuizQuestionWithAnswers, } from '@/types/quiz';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/db/queries/quizzes/quiz.ts` around lines 25 - 29, Add a single blank line immediately before the "export type { QuizAnswer, QuizQuestion, QuizQuestionWithAnswers } from '@/types/quiz';" re-export so it is visually separated from the preceding import list; locate the export type block (the "export type" statement) and insert one empty line above it to improve readability.frontend/components/home/FlipCardQA.tsx (1)
358-358: Preferfocus-visible:variants overfocus:for keyboard-only ring styling.
focus:outline-noneremoves the browser outline on both mouse clicks and keyboard navigation. The patternfocus-visible:ring-2 focus-visible:ring-(--accent-primary) focus-visible:ring-offset-2 focus-visible:outline-nonelimits the custom ring to keyboard focus only, which is the expected UX for dot-navigation controls.🔧 Suggested fix
- className="relative focus:ring-2 focus:ring-[var(--accent-primary)] focus:ring-offset-2 focus:outline-none" + className="relative focus-visible:ring-2 focus-visible:ring-(--accent-primary) focus-visible:ring-offset-2 focus-visible:outline-none"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/home/FlipCardQA.tsx` at line 358, In the FlipCardQA component replace the generic focus: utility classes on the interactive element (currently using className="... focus:ring-2 focus:ring-[var(--accent-primary)] focus:ring-offset-2 focus:outline-none") with their focus-visible: equivalents so the custom ring and outline removal apply only for keyboard focus; update the class references to focus-visible:ring-2, focus-visible:ring-[var(--accent-primary)], focus-visible:ring-offset-2 and focus-visible:outline-none while removing or avoiding plain focus: variants.frontend/components/dashboard/ProfileCard.tsx (1)
207-212: Pre-existing: change-name and change-password forms have stubonSubmithandlers.Both handlers call
setTimeout(() => setIsSaving(false), 1000)without sending any data to an API. The settings forms are visually complete but non-functional. This isn't introduced by this PR, but worth tracking as a follow-up.Would you like me to open a tracking issue or draft the implementation for the name/password update API calls?
Also applies to: 243-251
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/dashboard/ProfileCard.tsx` around lines 207 - 212, The change-name and change-password form onSubmit handlers currently only toggle setIsSaving and use a stub setTimeout; replace these stubs with real async API calls: call the appropriate profile update functions (e.g., an updateName/updateProfile API or changePassword API), await the response, validate inputs before sending, setIsSaving(true) before the call and setIsSaving(false) in finally, update local state on success (e.g., setUser or relevant state) and surface API errors to the user (set an error state or show notifications) instead of silently hiding the spinner; remove the setTimeout and ensure you reference the form handlers around the onSubmit in the ProfileCard component so both the change-name and change-password blocks behave correctly.frontend/components/admin/quiz/QuestionEditor.tsx (1)
402-402: Inconsistent CSS variable syntax — use Tailwind v4 parentheses shorthand.Lines 402 and 449 use
[var(--accent-primary)](arbitrary-value bracket syntax), while the rest of the codebase uses the Tailwind v4 native-(--accent-primary)shorthand (e.g.,statItemBaseinProfileCard.tsx, sponsor badge classes, etc.).🔧 Suggested fix
- <div className="rounded-lg border-2 border-[var(--accent-primary)]"> + <div className="rounded-lg border-2 border-(--accent-primary)">- className="border-border bg-background text-foreground w-full rounded-md border px-3 py-2 text-sm focus:ring-1 focus:ring-[var(--accent-primary)] focus:outline-none" + className="border-border bg-background text-foreground w-full rounded-md border px-3 py-2 text-sm focus:ring-1 focus:ring-(--accent-primary) focus:outline-none"Also applies to: 449-449
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/admin/quiz/QuestionEditor.tsx` at line 402, Replace the inconsistent arbitrary-value Tailwind usage in QuestionEditor.tsx: find the divs using className with "border-[var(--accent-primary)]" (e.g., the rounded-lg border-2 element and the other occurrence around line 449) and update them to the project's Tailwind v4 native CSS-variable shorthand (use border-(--accent-primary) instead) so they match other usages like statItemBase in ProfileCard.tsx and sponsor badge classes.frontend/lib/validation/quiz-publish-validation.ts (1)
12-12: Importlocalesfromfrontend/i18n/config.tsinstead of hardcoding.The local constant
const LOCALES = ['en', 'uk', 'pl']duplicates thelocalesexport fromfrontend/i18n/config.ts. Importing it ensures locale changes are reflected automatically.♻️ Suggested refactor
-const LOCALES = ['en', 'uk', 'pl']; +import { locales as LOCALES } from '@/i18n/config';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/lib/validation/quiz-publish-validation.ts` at line 12, Replace the hardcoded LOCALES array in quiz-publish-validation.ts with the canonical locales export from frontend/i18n/config.ts: remove const LOCALES = ['en','uk','pl'] and instead import { locales } (or the exported name) from that config file, then use that imported symbol wherever LOCALES is referenced (e.g., validation routines in this module) so locale changes in the config propagate automatically.frontend/components/dashboard/ActivityHeatmapCard.tsx (2)
82-86: Both branches of this conditional are identical.The
if (periodOffset === 0)andelseblocks both execute the samesetDate(getDate() + 1)call. This dead conditional can be simplified.Suggested simplification
- if (periodOffset === 0) { - windowEndExclusive.setDate(windowEndExclusive.getDate() + 1); - } else { - windowEndExclusive.setDate(windowEndExclusive.getDate() + 1); - } + windowEndExclusive.setDate(windowEndExclusive.getDate() + 1);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/dashboard/ActivityHeatmapCard.tsx` around lines 82 - 86, The conditional in ActivityHeatmapCard.tsx around periodOffset is dead code because both branches call windowEndExclusive.setDate(windowEndExclusive.getDate() + 1);; remove the if/else and replace with a single unconditional call to windowEndExclusive.setDate(windowEndExclusive.getDate() + 1); where the current conditional lives to simplify logic and avoid redundant code (search for periodOffset and windowEndExclusive in the file to locate the exact spot).
188-201: Unusedstreakandmaxvariables insideuseMemo.The memo computes
streakandmaxbut only returnstodayKey. The streak/max computation is dead code here (streak display uses thecurrentStreakprop instead).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/dashboard/ActivityHeatmapCard.tsx` around lines 188 - 201, The useMemo in ActivityHeatmapCard calculates unused variables streak and max by iterating heatmapData but only returns todayKey; remove the dead loop and variables and simplify the useMemo to only compute and return todayKey (keeping heatmapData in the dependency array), or if you intended to use streak/max instead, return them along with todayKey and update the component to consume currentStreak/currentMax; update the useMemo around todayKey, streak, and max accordingly.frontend/app/[locale]/quiz/[slug]/page.tsx (1)
8-12: Duplicate import source: two imports from'@/db/queries/quizzes/quiz'.
stripCorrectAnswers(Line 8) andgetQuizBySlug/getQuizQuestionsRandomized(Lines 9-12) both import from the same module. This works at runtime but could trigger linter warnings and is likely an artifact of the reformatting. Consider consolidating into a single import statement.Suggested consolidation
-import { stripCorrectAnswers } from '@/db/queries/quizzes/quiz'; -import { - getQuizBySlug, - getQuizQuestionsRandomized, -} from '@/db/queries/quizzes/quiz'; +import { + getQuizBySlug, + getQuizQuestionsRandomized, + stripCorrectAnswers, +} from '@/db/queries/quizzes/quiz';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/`[locale]/quiz/[slug]/page.tsx around lines 8 - 12, Consolidate the duplicate imports from '@/db/queries/quizzes/quiz' into a single statement: replace the two import blocks with one import that imports stripCorrectAnswers, getQuizBySlug, and getQuizQuestionsRandomized together; update the import that currently references stripCorrectAnswers and the import that references getQuizBySlug/getQuizQuestionsRandomized so only one combined import remains (referencing the symbols stripCorrectAnswers, getQuizBySlug, getQuizQuestionsRandomized).frontend/app/[locale]/admin/shop/orders/page.tsx (1)
65-80: Unnecessary optional chaining onorderinviewModelsmapping.
orderis an element fromitems.map(...)— it can never benull/undefined. The?.on lines 67 is superfluous.Suggested fix
- const totalMinor = pickMinor(order?.totalAmountMinor, order?.totalAmount); + const totalMinor = pickMinor(order.totalAmountMinor, order.totalAmount);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/`[locale]/admin/shop/orders/page.tsx around lines 65 - 80, The map callback creates viewModels from each order item so the optional chaining on the local variable is unnecessary; remove the `?.` usage when calling pickMinor and when accessing order.totalAmount (i.e., inside the mapping where pickMinor(order?.totalAmountMinor, order?.totalAmount) is used) and keep plain property access like pickMinor(order.totalAmountMinor, order.totalAmount); this is in the mapping that defines viewModels (referencing orderCurrency, formatDate, pickMinor, formatMoney, and t).frontend/app/[locale]/admin/shop/orders/[id]/page.tsx (1)
20-31: ExtractpickMinorandorderCurrencyinto a shared utility module.
pickMinoris duplicated identically acrossfrontend/app/[locale]/admin/shop/orders/page.tsx(lines 26–29) andfrontend/app/[locale]/admin/shop/orders/[id]/page.tsx(lines 20–23).orderCurrencyis also duplicated with functionally identical logic, though with different parameter type signatures. Extract both into a shared utility (e.g.,@/lib/shop/order-utils.ts) and update type signatures to accept the more flexible union type to serve both call sites.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/`[locale]/admin/shop/orders/[id]/page.tsx around lines 20 - 31, Extract the duplicated functions pickMinor and orderCurrency into a shared utility module (e.g., create a new module exporting pickMinor and orderCurrency), update their signatures to be flexible (pickMinor(minor: unknown, legacyMajor: unknown): number | null and orderCurrency(order: { currency?: string | null } | Partial<{currency?: string|null}>, locale: string): CurrencyCode or otherwise accept the union types used at both call sites), replace the duplicate implementations in both order pages with imports from the new utility, and remove the old local copies; ensure callers still import from the new module and that orderCurrency uses resolveCurrencyFromLocale and the same UAH/USD logic.
| <main className="mx-auto max-w-6xl px-4 py-8" aria-labelledby="order-title"> | ||
| <header className="flex items-start justify-between gap-4"> | ||
| <div className="min-w-0"> | ||
| <h1 id="order-title" className="text-foreground text-2xl font-bold"> | ||
| Order | ||
| </h1> | ||
| <p className="text-muted-foreground mt-1 font-mono text-xs break-all"> | ||
| {order.id} | ||
| </p> | ||
| </div> | ||
|
|
||
| <div className="flex shrink-0 flex-wrap items-center gap-2"> | ||
| <Link | ||
| href="/admin/shop/orders" | ||
| className="border-border text-foreground hover:bg-secondary rounded-md border px-3 py-1.5 text-sm font-medium transition-colors" | ||
| > | ||
| Back | ||
| </Link> | ||
|
|
||
| <main | ||
| className="mx-auto max-w-6xl px-4 py-8" | ||
| aria-labelledby="order-title" | ||
| <RefundButton orderId={order.id} disabled={!canRefund} /> | ||
| </div> | ||
| </header> |
There was a problem hiding this comment.
Hardcoded English strings throughout admin order detail page.
The sibling orders list page (orders/page.tsx) uses t('title'), t('table.*'), etc. for all user-facing text, but this detail page hardcodes "Order", "Back", "Summary", "Payment status", "Total", "Provider", "Payment intent", "Idempotency key", "Stock / timestamps", "Created", "Updated", "Stock restored", "Product", "Qty", "Unit", "Line total", "No items found…", and "Order items".
This creates an i18n gap — the page won't be translatable for non-English locales. Consider extracting these to a translation namespace (e.g. shop.admin.orderDetail.*), consistent with the pattern used in the list page.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/app/`[locale]/admin/shop/orders/[id]/page.tsx around lines 64 - 85,
The page.tsx admin order detail component hardcodes all user-facing strings
("Order", "Back", "Summary", etc.), breaking i18n; replace these literals with
calls to the app's translation function (e.g.,
t('shop.admin.orderDetail.title'), t('shop.admin.orderDetail.back'), etc.) and
load the same translation namespace used by the sibling orders list (ensure you
import/use the translation hook or server-side t loader used across admin
pages), updating all occurrences in the component (header text, buttons like
RefundButton label if present, section headings like "Summary", "Payment
status", "Stock / timestamps", table headers "Product", "Qty", "Unit", "Line
total", "No items found…", and "Order items") to use keys under a consistent
namespace such as shop.admin.orderDetail.*.
| const passwordBytesTooLong = | ||
| passwordTouched && | ||
| utf8ByteLength(passwordValue) > PASSWORD_MAX_BYTES; | ||
| passwordTouched && utf8ByteLength(passwordValue) > PASSWORD_MAX_BYTES; |
There was a problem hiding this comment.
Two error messages render simultaneously when the password exceeds the byte limit.
When passwordBytesTooLong is true, passwordPolicyOk is also false (line 40 catches the same condition), so both passwordErrorText (the generic "invalid password" paragraph at line 116) and the specific passwordTooLongBytes paragraph (lines 120-124) are visible at the same time. Confirm this double-display is intentional; if not, suppress passwordErrorText when the only failure reason is byte length.
💡 One way to suppress the generic message when bytes are the only failure
- const passwordErrorText =
- passwordTouched && !passwordPolicyOk
- ? tf('validation.invalidPassword', { passwordRequirementsText })
- : null;
+ const passwordBytesTooLong =
+ passwordTouched && utf8ByteLength(passwordValue) > PASSWORD_MAX_BYTES;
+
+ const passwordErrorText =
+ passwordTouched && !passwordPolicyOk && !passwordBytesTooLong
+ ? tf('validation.invalidPassword', { passwordRequirementsText })
+ : null;Also applies to: 115-125
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/components/auth/ResetPasswordForm.tsx` around lines 54 - 55, The
generic and specific password error messages both render when the password
exceeds byte limit; to fix, change the rendering condition for the generic
`passwordErrorText` so it is suppressed when `passwordBytesTooLong` is the only
failing condition. In practice, update the JSX/conditional that renders
`passwordErrorText` (the paragraph using `passwordPolicyOk`) to require
`passwordTouched && !passwordPolicyOk && !passwordBytesTooLong` (or equivalent)
so `passwordTooLongBytes` (the specific message driven by
`passwordBytesTooLong`, `PASSWORD_MAX_BYTES`, `utf8ByteLength`, and
`passwordTouched`) displays alone when byte-length is the sole failure.
| {tooltip.count === 0 | ||
| ? 'No activity' | ||
| : `${tooltip.count} ${tooltip.count === 1 ? 'attempt' : 'attempts'}`} |
There was a problem hiding this comment.
Hard-coded English strings bypass the i18n system.
The tooltip text "No activity", "attempt", and "attempts" are not using useTranslations. Similarly, "active day" / "active days" on Line 661 are hard-coded. All other user-facing text in this component correctly uses t().
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/components/dashboard/ActivityHeatmapCard.tsx` around lines 615 -
617, Replace the three hard-coded tooltip strings in ActivityHeatmapCard with
translations via the existing useTranslations hook: change "No activity" to
t('...') (e.g., t('noActivity')), and replace singular/plural
"attempt"/"attempts" and "active day"/"active days" with pluralized calls (e.g.,
t.rich or t with count/vars) so the component uses t(count, { count }) or an
appropriate plural key; update the translation keys rather than leaving literal
strings so all user-facing text uses the t() function in the ActivityHeatmapCard
render/tooltip logic.
| const [currentText, setCurrentText] = useState(t('cta')); | ||
| const [variantIndex, setVariantIndex] = useState(0); | ||
| const [variantIndex, setVariantIndex] = useState(0); | ||
| const [isFirstRender, setIsFirstRender] = useState(true); |
There was a problem hiding this comment.
Remove the unused isFirstRender state — it's dead code that triggers an unnecessary re-render on mount.
isFirstRender is initialised to true, flipped to false inside a mount-only useEffect, and never read anywhere in the component. The only effect is an extra state update (and re-render) on every mount.
🐛 Proposed fix
- const [isFirstRender, setIsFirstRender] = useState(true);- useEffect(() => {
- setIsFirstRender(false);
- }, []);
-Also applies to: 38-40
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/components/home/InteractiveCTAButton.tsx` at line 24, Remove the
dead state used for tracking initial render in the InteractiveCTAButton
component: delete the useState call that declares isFirstRender and
setIsFirstRender and remove the mount-only useEffect that flips it (references
to useState/useEffect and the identifiers isFirstRender/setIsFirstRender).
Ensure no other code reads isFirstRender before removing; if anything depends on
initial-mount behavior, replace it with a ref (useRef) or logic that does not
cause a state update on mount. Also remove any duplicate unused occurrences of
the same pattern elsewhere in the component (the other
isFirstRender/setIsFirstRender usage).
| <AccordionPrimitive.Content | ||
| data-slot="accordion-content" | ||
| className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down motion-reduce:!animate-none overflow-hidden text-sm" | ||
| className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm motion-reduce:!animate-none" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate and check the tailwindcss version in the project
find . -name "package.json" -o -name "package-lock.json" -o -name "yarn.lock" -o -name "pnpm-lock.yaml" | head -20Repository: DevLoversTeam/devlovers.net
Length of output: 172
🌐 Web query:
Tailwind CSS v4 important modifier syntax prefix vs suffix breaking change
💡 Result:
In Tailwind CSS v4, the important modifier moved from a prefix to a suffix:
- v3 (old / now deprecated in v4):
hover:!bg-red-500 - v4 (preferred):
hover:bg-red-500!(tailwindcss.com)
So, the ! now goes at the very end of the full class, after the utility (and after any arbitrary values/modifiers), e.g.:
<div class="flex! bg-red-500! hover:bg-red-600/50!"></div>
``` ([tailwindcss.com](https://tailwindcss.com/docs/upgrade-guide?utm_source=openai))
The **old prefix form still works for compatibility but is deprecated** in v4. ([tailwindcss.com](https://tailwindcss.com/docs/upgrade-guide?utm_source=openai))
Citations:
- 1: https://tailwindcss.com/docs/upgrade-guide?utm_source=openai
- 2: https://tailwindcss.com/docs/upgrade-guide?utm_source=openai
- 3: https://tailwindcss.com/docs/upgrade-guide?utm_source=openai
---
🌐 Web query:
`Tailwind CSS v4 ! modifier position animate-none motion-reduce`
💡 Result:
In **Tailwind CSS v4**, the **important (`!`) modifier goes at the very end of the class name** (after the utility and after any variants). The old v3-style `!`-prefix is still supported but **deprecated**. [1]
So for reduced-motion animation:
- Normal reduced-motion variant (no `!important`):
`motion-reduce:animate-none` (uses the `motion-reduce` variant + `animate-none`). [2]
- Reduced-motion + `!important` in v4:
`motion-reduce:animate-none!` [1]
- Deprecated v3-style equivalent (avoid in v4):
`motion-reduce:!animate-none` [1]
**Examples**
```html
<!-- Disable animation when the user prefers reduced motion -->
<div class="motion-reduce:animate-none"></div>
<!-- Force it with !important (v4 syntax) -->
<div class="motion-reduce:animate-none!"></div>Sources: Tailwind upgrade guide (important modifier change) [1], Tailwind animation docs (motion-reduce usage, animate-none) [2], Tailwind Labs discussion confirming postfix ! as the new standard [3].
🏁 Script executed:
# Check tailwindcss version in frontend package.json
cat frontend/package.json | grep -A 2 -B 2 tailwindcssRepository: DevLoversTeam/devlovers.net
Length of output: 414
🏁 Script executed:
# Verify the actual code at the mentioned line
cat -n frontend/components/ui/accordion.tsx | sed -n '55,70p'Repository: DevLoversTeam/devlovers.net
Length of output: 696
Update to Tailwind v4 important modifier syntax: motion-reduce:animate-none!
The project uses Tailwind CSS v4. The important modifier (!) syntax changed in v4 — it must now be a suffix, not a prefix:
| v3 (deprecated in v4) | v4 (current best practice) |
|---|---|
!animate-none |
animate-none! |
While Tailwind v4 still supports the old prefix syntax for backward compatibility, it is deprecated. Update this line to follow v4 conventions:
Proposed fix
- className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm motion-reduce:!animate-none"
+ className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm motion-reduce:animate-none!"This ensures the animation override respects prefers-reduced-motion: reduce per WCAG 2.1 §2.3.3.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm motion-reduce:!animate-none" | |
| className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm motion-reduce:animate-none!" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/components/ui/accordion.tsx` at line 61, Replace the deprecated
Tailwind important-prefix syntax in the className on the accordion content
element: change the modifier "motion-reduce:!animate-none" to the v4 suffix form
"motion-reduce:animate-none!" in frontend/components/ui/accordion.tsx (look for
the className string containing data-[state=closed]:animate-accordion-up
data-[state=open]:animate-accordion-down overflow-hidden text-sm
motion-reduce:!animate-none).
Summary by CodeRabbit
Release Notes
New Features
Improvements
Dependencies
Refactor