feat(Blog):Adding pagination#244
Conversation
….org JSON‑LD for Article (BlogPosting) and BreadcrumbList , Added <time datetime> tags where blog dates renders
merging develop
…he BreadcrumbList, logo to the cocial links in User info, Fixed: main container alignment, category navigation in breadcrumbs
…he BreadcrumbList, logo to the cocial links in User info, Fixed: main container alignment, category navigation in breadcrumbs
…, fied breadcrumbs category translaion, added category to the recommended cards, fixed search for localisations
merging develop
merging develop
merging develop
merging develop
feat(Blog): merging develop
merging develop
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds Upstash Redis-backed QA caching and client-side blog pagination; centralizes shop UI class tokens; introduces many Next.js page metadata exports; refactors cart into a client component; and applies widespread styling and localization updates across shop and blog components. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Browser
participant API as Questions API
participant Cache as QA Cache
participant Redis as Upstash Redis
participant DB as Database
Client->>API: GET /api/questions/[category]?page=1&search=...
API->>Cache: buildQaCacheKey(category, locale, page, limit, search)
Cache->>Redis: getQaCache(key)
alt Cache HIT
Redis-->>Cache: cached data
Cache-->>API: QaApiResponse (cached)
API-->>Client: 200 + x-qa-cache: HIT
else Cache MISS
Redis-->>Cache: null
Cache->>DB: Query questions (category, filters)
DB-->>Cache: items + total count
Cache->>Redis: setQaCache(key, {items, total}, ttl=12h)
Cache-->>API: QaApiResponse (fresh)
API-->>Client: 200 + x-qa-cache: MISS
end
sequenceDiagram
participant User as User
participant Filter as BlogFilters (state)
participant Paginator as BlogPagination
participant Router as Next Router
participant View as Rendered Posts
User->>Filter: Change author/category/search
Filter->>Filter: Reset currentPage to 1
Filter->>Router: updatePageParam(1)
Router->>Filter: URL updated
Filter->>Filter: Compute paginatedPosts & totalPages
User->>Paginator: Click page number
Paginator->>Filter: onPageChange(newPage)
Filter->>Filter: Update currentPage
Filter->>Router: updatePageParam(newPage)
Filter->>View: Render paginatedPosts[currentPage]
View-->>User: Display paginated results
Filter->>Filter: scrollToCategoryFilters()
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 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: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/app/api/questions/[category]/route.ts (1)
90-92:⚠️ Potential issue | 🟡 MinorPotential SQL injection via
searchparameter.The
searchvariable is interpolated directly into theilikepattern without escaping SQL wildcards (%,_). A malicious search string could manipulate the query pattern. Whileilikewith parameterized queries in Drizzle should prevent classic SQL injection, the wildcard characters in user input can still cause unexpected matching behavior.Consider escaping SQL wildcards in the search string:
🛡️ Proposed fix to escape wildcards
+function escapeIlike(str: string) { + return str.replace(/[%_]/g, '\\$&'); +} + const whereCondition = search - ? and(baseCondition, ilike(questionTranslations.question, `%${search}%`)) + ? and(baseCondition, ilike(questionTranslations.question, `%${escapeIlike(search)}%`)) : baseCondition;
🤖 Fix all issues with AI agents
In `@frontend/.env.example`:
- Around line 11-13: The environment variable entries are out of the expected
order; swap the two keys so UPSTASH_REDIS_REST_TOKEN appears before
UPSTASH_REDIS_REST_URL in the .env example, i.e., reorder the lines so
UPSTASH_REDIS_REST_TOKEN= comes first and UPSTASH_REDIS_REST_URL= follows,
ensuring the dotenv-linter ordering check passes while keeping the same variable
names.
In `@frontend/app/`[locale]/shop/cart/CartPageClient.tsx:
- Around line 70-78: In translateColor, remove the dead try/catch and use
tColors.has to only call tColors(colorSlug) when a translation exists; update
the function (translateColor and usage of tColors) to: if no color return null,
compute colorSlug, if tColors.has(colorSlug) return tColors(colorSlug) else
return the original color string so you don't rely on a thrown error from
next-intl.
In `@frontend/app/`[locale]/shop/checkout/success/page.tsx:
- Around line 24-28: The metadata.title string in the exported metadata object
currently reads "Order Confirmed| DevLovers" and is missing a space before the
pipe; update the title value in the metadata constant (export const metadata) to
include the space so it reads "Order Confirmed | DevLovers" to fix the typo.
In `@frontend/app/`[locale]/shop/orders/error.tsx:
- Around line 25-34: Replace the hardcoded English strings with next-intl
translations: import useTranslations from 'next-intl' in the component, call
const t = useTranslations('orders') (or appropriate namespace), then replace
"Orders" with t('title'), "Failed to load orders." with t('loadError') and "Try
again" with t('tryAgain'); keep the existing SHOP_OUTLINE_BTN class and the
reset onClick handler unchanged so only the displayed strings are swapped to
t(...) keys.
In `@frontend/app/`[locale]/shop/products/[slug]/page.tsx:
- Around line 14-17: The file currently exports a static metadata const with
placeholder text; replace it with an exported async generateMetadata function
that receives the route params (params.slug), fetches the product data for that
slug, and returns a Metadata object containing the real title and description
(e.g., `${product.name} | DevLovers` and product.description). Remove or stop
exporting the static export const metadata and ensure generateMetadata handles
errors/missing product by returning a sensible fallback metadata. Use the symbol
generateMetadata and reference params.slug and the existing product-fetching
utility you have in the codebase to implement this.
In `@frontend/components/blog/BlogFilters.tsx`:
- Around line 670-671: The expression "{selectedCategory && null}" in
BlogFilters.tsx is dead code and should be removed or replaced with the intended
conditional rendering; locate the JSX in the BlogFilters component where
"selectedCategory" is used and either delete that no-op expression or implement
the missing render (e.g., render a badge, button, or fragment) using
"selectedCategory" so the conditional actually outputs meaningful JSX.
In `@frontend/components/blog/BlogPagination.tsx`:
- Around line 23-31: The initial useState(false) in the BlogPagination component
can cause an SSR/client hydration mismatch; change initialization and render
logic so the initial render matches the client or delays rendering until after
mount: either initialize isMobile with a safe check (useState(() => typeof
window !== 'undefined' && window.matchMedia('(max-width: 640px)').matches)) so
the initial client render reflects the actual viewport, or add a mounted guard
(e.g., isMounted ref/state) and skip rendering the pagination controls until
after the useEffect runs; update the useEffect (media, update) and cleanup logic
accordingly to keep behavior identical once mounted.
In `@frontend/components/shared/HeaderButton.tsx`:
- Around line 134-136: In wrapWithBadge, the current guard uses a falsy check
(if (!badge)) which incorrectly treats valid badges like 0 or '' as absent;
change this to a nullish check (e.g., if (badge == null)) so only null/undefined
skip rendering the badge, keeping valid numeric/empty-string badges visible when
wrapping the node.
In `@frontend/components/shop/product-sort.tsx`:
- Around line 73-102: The select element used to render sorting (the <select>
with id={selectId}, name="sort", value={currentSort} and onChange calling
handleSort) lacks the "peer" utility class so the ChevronDown icon's peer-hover
and peer-focus-visible selectors never trigger; add the "peer" class to the
select's className (alongside SHOP_SELECT_BASE, SHOP_SELECT_INTERACTIVE,
SHOP_FOCUS, SHOP_DISABLED and the conditional isActive text class) so the
ChevronDown (component ChevronDown) responds to hover/focus as intended.
In `@frontend/lib/cache/qa.ts`:
- Around line 27-99: All Redis calls in getQaCache, setQaCache,
invalidateQaCacheByCategory, and invalidateAllQaCache must be wrapped in
try/catch so client errors don't bubble up; update each function to wrap the
Redis operations (you can wrap the whole function body after retrieving
getRedisClient()) and on error return safe fallbacks: getQaCache -> null,
setQaCache -> silently return (no throw),
invalidateQaCacheByCategory/invalidateAllQaCache -> return 0; include the error
in a console.warn/log statement for observability but do not rethrow.
In `@frontend/package.json`:
- Line 3: Remove the pinned Netlify plugin entry for
`@netlify/plugin-nextjs`@5.15.7 to allow Netlify's OpenNext adapter to manage the
correct adapter for Next.js 16: edit frontend/package.json to delete the
explicit `@netlify/plugin-nextjs` dependency (and any version lock) and also
remove corresponding configuration blocks in netlify.toml that reference that
legacy plugin so Netlify can auto-select the OpenNext adapter; ensure no other
code assumes the legacy plugin name.
In `@README.md`:
- Line 3: The README contains a stray unformatted token "develop" that is
unclear; either remove that standalone word or convert it into a proper Markdown
heading (e.g., change "develop" to "## develop" or another appropriate heading
text) so it clearly labels the Netlify deployment badge or add surrounding
context text explaining what "develop" refers to; update the README.md content
accordingly.
🧹 Nitpick comments (18)
frontend/app/api/questions/[category]/route.ts (1)
136-142: Cache write is fire-and-forget; failures are silent.
setQaCacheis awaited but any errors thrown will propagate to the catch block and return a 500 error to the client, even though the data was successfully retrieved from the database. Consider wrapping the cache write in a try-catch or making it non-blocking to avoid failing the request due to cache issues.♻️ Proposed fix for resilient cache write
- await setQaCache(cacheKey, { + setQaCache(cacheKey, { items, total, page, totalPages, locale, - }); + }).catch(err => console.warn('[qa-cache] Failed to set cache', err));frontend/lib/shop/ui-classes.ts (1)
1-7: Template literal class strings contain embedded newlines.Multi-line template literals include newline characters in the resulting string. While
cn()(usingclsx+twMerge) handles this gracefully, using these constants directly (withoutcn()) may produce unexpected whitespace in the DOMclassattribute. This is typically harmless but could cause minor issues with class string comparisons or debugging.If consistency is desired, consider single-line strings or trimming:
♻️ Alternative: single-line strings
-export const SHOP_FOCUS = ` - focus-visible:outline-none - focus-visible:ring-2 - focus-visible:ring-[color:var(--color-ring)] - focus-visible:ring-offset-2 - focus-visible:ring-offset-background -`; +export const SHOP_FOCUS = + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--color-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-background';frontend/app/[locale]/shop/orders/error.tsx (1)
10-13: Theerrorprop is declared but unused.The
ErrorPagePropstype includes anerrorprop with an optionaldigest, but the component doesn't use it. Consider logging the error for observability or removing it from the destructured props if intentionally unused.frontend/components/shop/shop-hero.tsx (1)
1-14: Imports placed after interface definition.The import statement for UI classes (lines 8-14) appears after the
HeroPropsinterface definition. Conventionally, all imports should be grouped at the top of the file before any declarations.♻️ Proposed reordering
import { Link } from '@/i18n/routing'; +import { + SHOP_CTA_BASE, + SHOP_CTA_INSET, + SHOP_CTA_WAVE, + SHOP_FOCUS, + shopCtaGradient, +} from '@/lib/shop/ui-classes'; + interface HeroProps { headline: string; subheadline: string; ctaText: string; ctaLink: string; } -import { - SHOP_CTA_BASE, - SHOP_CTA_INSET, - SHOP_CTA_WAVE, - SHOP_FOCUS, - shopCtaGradient, -} from '@/lib/shop/ui-classes';frontend/app/[locale]/shop/page.tsx (2)
54-65: "View All" link uses inline classes instead of centralized tokens.The "View All" link has extensive inline Tailwind classes while other buttons in this PR use the centralized
SHOP_OUTLINE_BTN_*tokens. Consider using the existing tokens for consistency:♻️ Proposed refactor using tokens
<Link href="/shop/products?filter=new" - className=" - group inline-flex items-center gap-2 rounded-md border border-border - px-4 py-2 - text-xs sm:text-sm font-semibold tracking-[0.25em] uppercase - text-muted-foreground hover:text-foreground - bg-transparent - shadow-none hover:shadow-[var(--shop-card-shadow-hover)] - transition-[transform,box-shadow,color,filter] duration-500 ease-out - hover:-translate-y-0.5 hover:brightness-110 - focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--color-ring)] - focus-visible:ring-offset-2 focus-visible:ring-offset-background - " + className={cn( + SHOP_OUTLINE_BTN_BASE, + SHOP_OUTLINE_BTN_INTERACTIVE, + SHOP_FOCUS, + 'gap-2 text-muted-foreground hover:text-foreground' + )} aria-label={t('viewAll')} >
139-164: Duplicate CTA pattern with shop-hero.tsx.The layered CTA structure (base gradient → hover wave → glass inset → content) is duplicated between this file and
shop-hero.tsx. Consider extracting this into a reusableShopCtaorHeroCtaInnercomponent to reduce duplication and ensure consistent updates.frontend/app/[locale]/shop/checkout/payment/StripePaymentClient.tsx (1)
102-143: Consider extractingHeroCtaInnerto a shared component.This component is duplicated in
CartPageClient.tsx(lines 172-191, 396-418) with the same gradient overlay pattern. Extracting it to a shared location (e.g., alongsideui-classes.ts) would reduce duplication and ensure consistent CTA styling across the shop.frontend/app/[locale]/shop/cart/CartPageClient.tsx (1)
104-115: Consider validating response content-type before parsing JSON.If the server returns a non-JSON response (e.g., HTML error page from a proxy),
response.json()will throw a parsing error that gets caught generically. A more defensive approach would handle this explicitly.Proposed enhancement
const response = await fetch('/api/shop/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey, }, body: JSON.stringify({ items: cart.items.map(item => ({ productId: item.productId, quantity: item.quantity, selectedSize: item.selectedSize, selectedColor: item.selectedColor, })), }), }); + const contentType = response.headers.get('content-type'); + if (!contentType?.includes('application/json')) { + setCheckoutError('Unexpected server response. Please try again.'); + return; + } + const data = await response.json();frontend/app/[locale]/shop/orders/page.tsx (1)
189-199: Consider moving link style constants outside the component function.
NAV_LINKandORDER_HEADLINE_LINKare computed on every request. Since they don't depend on runtime values, they could be defined at module scope for a minor performance improvement and cleaner code.Proposed refactor
+const NAV_LINK = cn(SHOP_NAV_LINK_BASE, 'text-lg', SHOP_FOCUS); +const ORDER_HEADLINE_LINK = cn( + 'block max-w-[24rem] truncate', + SHOP_LINK_BASE, + SHOP_LINK_MD, + SHOP_FOCUS +); + export default async function MyOrdersPage({ params, }: { params: Promise<{ locale: string }>; }) { // ... existing code ... - const NAV_LINK = cn(SHOP_NAV_LINK_BASE, 'text-lg', SHOP_FOCUS); - - const ORDER_HEADLINE_LINK = cn( - 'block max-w-[24rem] truncate', - SHOP_LINK_BASE, - SHOP_LINK_MD, - SHOP_FOCUS - );frontend/app/[locale]/shop/products/[slug]/page.tsx (2)
44-46: Avoid usinganytype forproduct.Casting to
anyloses type safety and can hide bugs. Consider defining a union type or using type guards to handle the different shapes for available vs unavailable products.Suggested approach
// Define the expected shape or use discriminated union type AvailableProduct = { name: string; image?: string; badge?: string; price: number; originalPrice?: number; currency: CurrencyCode; description?: string; }; // Then use proper type guards const product = result.product;
69-78: Condition checksproduct.badgebut rendersbadgeLabel.Line 69 conditionally renders based on
product.badge, but the badge label is derived from thebadgevariable (line 47). While functionally equivalent in this case, using consistent references improves readability.- {product.badge && ( + {badgeLabel && ( <span className={`absolute left-4 top-4 z-10 rounded px-2 py-1 text-xs font-semibold uppercase ${ badge === 'SALE'frontend/app/[locale]/shop/orders/[id]/page.tsx (1)
216-222: Avoid!importantmodifiers in Tailwind classes.The
!underline !decoration-2 !underline-offset-4classes suggest a specificity conflict with the importedSHOP_LINK_BASEorSHOP_LINK_MDtokens. Consider adjusting the base token definitions instead of using!importantoverrides.Investigation suggestion
Check if
SHOP_LINK_BASEorSHOP_LINK_MDsetsno-underlineor conflicting decoration styles. If so, either:
- Create a separate token for underlined links
- Adjust the base token to not set text-decoration
- Use a more specific composition that doesn't require
!importantconst PRODUCT_LINK = cn( SHOP_LINK_BASE, SHOP_LINK_MD, SHOP_FOCUS, - 'truncate', - '!underline !decoration-2 !underline-offset-4' + 'truncate underline decoration-2 underline-offset-4' );If the conflict persists, the base tokens may need adjustment.
frontend/components/shop/product-filters.tsx (1)
148-149: Consider using English for inline code comments.The comments on lines 148-149 and 186-187 are in Ukrainian. For consistency and broader team accessibility, consider using English for code comments.
SHOP_SWATCH_BASE, - // у фільтрах розмір 8x8, тому override: + // filters use 8x8 size, override default: 'h-8 w-8',SHOP_SIZE_CHIP_BASE, - // у фільтрах інші паддінги — override: + // filters use different padding — override: 'px-3 py-1.5 text-sm',Also applies to: 186-187
frontend/app/[locale]/shop/checkout/success/page.tsx (1)
60-104: Consider extractingHeroCtaInnerand CTA constants to a shared location.
SHOP_HERO_CTA_SM,SHOP_OUTLINE_BTN, andHeroCtaInnerare defined in this file but appear to be duplicated incheckout/error/page.tsxas well. Extracting these to a shared module (e.g., alongsidefrontend/lib/shop/ui-classes.ts) would reduce duplication and ensure consistency across checkout pages.frontend/app/[locale]/shop/checkout/error/page.tsx (2)
216-221: Avoidas anytype casting; prefer proper type narrowing.The repeated
(order as any).totalAmountMinorand(order as any).currencycasts suggest the return type ofgetOrderSummarymay not include these fields. This bypasses TypeScript's type checking and could mask runtime errors.Consider updating the
getOrderSummaryreturn type to include these fields, or use optional chaining with proper type guards.🛠️ Suggested approach
- const totalMinor = - typeof (order as any).totalAmountMinor === 'number' - ? (order as any).totalAmountMinor - : null; - - const currency = (order as any).currency ?? resolveCurrencyFromLocale(locale); + const totalMinor = + 'totalAmountMinor' in order && typeof order.totalAmountMinor === 'number' + ? order.totalAmountMinor + : null; + + const currency = + 'currency' in order && typeof order.currency === 'string' + ? order.currency + : resolveCurrencyFromLocale(locale);
48-62: Duplicate CTA constants across checkout pages.
SHOP_HERO_CTA_SMandSHOP_OUTLINE_BTNare defined identically here and incheckout/success/page.tsx. As noted in the success page review, extracting these to a shared module would improve maintainability.frontend/components/blog/BlogFilters.tsx (2)
329-332: Effect may cause extra re-renders due tocurrentPagein dependency array.This effect syncs
currentPagestate withparsedPagefrom URL. However, includingcurrentPagein the dependency array means the effect runs whenever either value changes. SincesetCurrentPageinside the effect changescurrentPage, this could cause an extra render cycle.🛠️ Suggested fix - remove currentPage from dependencies
useEffect(() => { - if (currentPage === parsedPage) return; setCurrentPage(parsedPage); - }, [currentPage, parsedPage]); + }, [parsedPage]);The early return prevents an infinite loop, but the effect still runs unnecessarily when
currentPagechanges from other sources (likesetCurrentPageinonPageChange).
460-464: Potential for effect loop when clamping page.When
currentPage > totalPages, this effect setscurrentPagetototalPagesand callsupdatePageParam. IfupdatePageParamtriggers a URL change that causesparsedPageto update, the sync effect at lines 329-332 could fire, potentially causing multiple re-renders.The logic is safe from infinite loops due to the
currentPage <= totalPagesguard, but consider consolidating page clamping logic into the filter-change effect (lines 348-375) to reduce effect interdependencies.
| # --- Upstash Redis (REST) | ||
| UPSTASH_REDIS_REST_URL= | ||
| UPSTASH_REDIS_REST_TOKEN= |
There was a problem hiding this comment.
Reorder Upstash env keys to satisfy dotenv-linter.
The linter warning will persist until the keys are ordered as expected. Swapping them here keeps the example aligned with lint rules.
♻️ Proposed fix
- UPSTASH_REDIS_REST_URL=
- UPSTASH_REDIS_REST_TOKEN=
+ UPSTASH_REDIS_REST_TOKEN=
+ UPSTASH_REDIS_REST_URL=📝 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.
| # --- Upstash Redis (REST) | |
| UPSTASH_REDIS_REST_URL= | |
| UPSTASH_REDIS_REST_TOKEN= | |
| # --- Upstash Redis (REST) | |
| UPSTASH_REDIS_REST_TOKEN= | |
| UPSTASH_REDIS_REST_URL= |
🧰 Tools
🪛 dotenv-linter (4.0.0)
[warning] 13-13: [UnorderedKey] The UPSTASH_REDIS_REST_TOKEN key should go before the UPSTASH_REDIS_REST_URL key
(UnorderedKey)
🤖 Prompt for AI Agents
In `@frontend/.env.example` around lines 11 - 13, The environment variable entries
are out of the expected order; swap the two keys so UPSTASH_REDIS_REST_TOKEN
appears before UPSTASH_REDIS_REST_URL in the .env example, i.e., reorder the
lines so UPSTASH_REDIS_REST_TOKEN= comes first and UPSTASH_REDIS_REST_URL=
follows, ensuring the dotenv-linter ordering check passes while keeping the same
variable names.
| const translateColor = (color: string | null | undefined): string | null => { | ||
| if (!color) return null; | ||
| const colorSlug = color.toLowerCase(); | ||
| try { | ||
| return tColors(colorSlug); | ||
| } catch { | ||
| return color; | ||
| } | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
next-intl useTranslations missing keys default behavior throw error
💡 Result:
In next-intl, when you call useTranslations() / t('someKey') and the message can’t be resolved (e.g. missing key), the default behavior is not to throw: it logs an error to the console and renders the message path (${namespace}.${key}) so the app keeps running. You can customize both the logging and the rendered fallback via onError and getMessageFallback. [1]
If you want to avoid the missing-key path and/or hard-fail, you can:
- Pre-check with
t.has('key')before callingt('key')(useful for optional messages). [2] - Throw on missing by customizing
onError(e.g. throw whenerror.code === IntlErrorCode.MISSING_MESSAGE) and optionally customize the UI fallback withgetMessageFallback. [1]
Also, since next-intl 3.3.1, missing-message errors include the full namespace + key in the error output (helpful for debugging). [3]
Sources:
[1] next-intl docs – Request configuration / Error handling (onError, getMessageFallback) (next-intl.dev)
[2] next-intl docs – Translations (t.has) (next-intl.dev)
[3] next-intl GitHub Discussion #704 (note about next-intl@3.3.1) (github.com)
Citations:
- 1: https://next-intl.dev/docs/usage/configuration
- 2: https://next-intl.dev/docs/usage/translations
- 3: Is there a way to see which message is missing upon the 'MISSING_MESSAGE' error? amannn/next-intl#704
🏁 Script executed:
# Find i18n or translation configuration files
find . -type f \( -name "*i18n*" -o -name "*translation*" -o -name "*config*" \) | head -20Repository: DevLoversTeam/devlovers.net
Length of output: 430
🏁 Script executed:
# Search for next-intl setup or configuration
rg "next-intl" --type ts --type tsx -A 3 -B 3 | head -50Repository: DevLoversTeam/devlovers.net
Length of output: 98
🏁 Script executed:
# Look at the specific file to understand context
cat -n frontend/app/[locale]/shop/cart/CartPageClient.tsx | head -100Repository: DevLoversTeam/devlovers.net
Length of output: 3578
🏁 Script executed:
cat -n frontend/i18n/config.tsRepository: DevLoversTeam/devlovers.net
Length of output: 393
🏁 Script executed:
# Search for next-intl configuration in the frontend directory
find frontend -type f -name "*.ts" -o -name "*.tsx" -o -name "*.js" | xargs grep -l "IntlError\|onError\|getMessageFallback" 2>/dev/null | head -10Repository: DevLoversTeam/devlovers.net
Length of output: 283
🏁 Script executed:
# Check the app layout or root config for next-intl setup
find frontend -type f \( -name "layout.tsx" -o -name "middleware.ts" \) | head -5Repository: DevLoversTeam/devlovers.net
Length of output: 127
🏁 Script executed:
# Look for any i18n setup in the app directory
fd -e "ts" -e "tsx" frontend/app | grep -i intl | head -10Repository: DevLoversTeam/devlovers.net
Length of output: 424
🏁 Script executed:
# Check the root layout file for next-intl configuration
cat -n frontend/app/layout.tsx | head -50Repository: DevLoversTeam/devlovers.net
Length of output: 1684
🏁 Script executed:
# Check the locale-specific layout
cat -n frontend/app/[locale]/layout.tsx | head -50Repository: DevLoversTeam/devlovers.net
Length of output: 1932
🏁 Script executed:
# Search for middleware that might configure next-intl error handling
find frontend -name "middleware.ts" -o -name "middleware.js" | xargs cat -n 2>/dev/nullRepository: DevLoversTeam/devlovers.net
Length of output: 53
🏁 Script executed:
# Search for any IntlProvider or error handling setup related to next-intl
rg "onError|getMessageFallback|IntlError" frontend/ --type ts --type tsxRepository: DevLoversTeam/devlovers.net
Length of output: 98
🏁 Script executed:
# Check if there's a messages/translations file to see the structure
find frontend -type f \( -name "messages.json" -o -name "en.json" -o -name "colors*" \) | head -10Repository: DevLoversTeam/devlovers.net
Length of output: 96
🏁 Script executed:
# Check the messages structure to understand color translations
cat frontend/messages/en.json | jq '.shop.catalog.colors' 2>/dev/null || cat frontend/messages/en.json | head -100Repository: DevLoversTeam/devlovers.net
Length of output: 179
🏁 Script executed:
# Search with correct syntax for next-intl error handling
rg "onError|getMessageFallback|IntlError" frontend --type tsRepository: DevLoversTeam/devlovers.net
Length of output: 3110
🏁 Script executed:
# Check the actual implementation of NextIntlClientProvider to confirm no custom error handling
cat -n frontend/app/[locale]/layout.tsx | grep -A 20 "NextIntlClientProvider"Repository: DevLoversTeam/devlovers.net
Length of output: 1800
Remove the try/catch; next-intl doesn't throw on missing translation keys.
next-intl by default logs an error to the console and returns the message path (e.g., "shop.catalog.colors.unknowncolor") when a key is missing—it does not throw. Since there's no custom onError handler configured in your NextIntlClientProvider, the catch block will never execute, and the fallback logic is dead code. If a color translation is missing, tColors(colorSlug) will return the message path string rather than the original color value.
Either ensure all colors are translated, or precheck with tColors.has(colorSlug) before calling tColors(colorSlug).
🤖 Prompt for AI Agents
In `@frontend/app/`[locale]/shop/cart/CartPageClient.tsx around lines 70 - 78, In
translateColor, remove the dead try/catch and use tColors.has to only call
tColors(colorSlug) when a translation exists; update the function
(translateColor and usage of tColors) to: if no color return null, compute
colorSlug, if tColors.has(colorSlug) return tColors(colorSlug) else return the
original color string so you don't rely on a thrown error from next-intl.
| export const metadata: Metadata = { | ||
| title: 'Order Confirmed| DevLovers', | ||
| description: | ||
| 'Your order has been placed. You can track its status on the order page.', | ||
| }; |
There was a problem hiding this comment.
Minor typo in metadata title.
The title is missing a space before the pipe separator.
✏️ Suggested fix
export const metadata: Metadata = {
- title: 'Order Confirmed| DevLovers',
+ title: 'Order Confirmed | DevLovers',
description:
'Your order has been placed. You can track its status on the order page.',
};🤖 Prompt for AI Agents
In `@frontend/app/`[locale]/shop/checkout/success/page.tsx around lines 24 - 28,
The metadata.title string in the exported metadata object currently reads "Order
Confirmed| DevLovers" and is missing a space before the pipe; update the title
value in the metadata constant (export const metadata) to include the space so
it reads "Order Confirmed | DevLovers" to fix the typo.
| <h1 className="text-2xl font-semibold">Orders</h1> | ||
| </header> | ||
|
|
||
| <section className="mt-6 rounded-lg border border-border bg-card p-5"> | ||
| <p className="text-sm text-muted-foreground">Failed to load orders.</p> | ||
|
|
||
| <div className="mt-6 rounded-md border p-4"> | ||
| <p className="text-sm">Failed to load orders.</p> | ||
| <button | ||
| type="button" | ||
| className="mt-3 text-sm underline underline-offset-4" | ||
| onClick={() => reset()} | ||
| > | ||
| Try again | ||
| </button> | ||
| </div> | ||
| <div className="mt-4 flex flex-wrap items-center gap-3"> | ||
| <button type="button" className={SHOP_OUTLINE_BTN} onClick={reset}> | ||
| Try again | ||
| </button> |
There was a problem hiding this comment.
Hardcoded English strings bypass i18n.
The strings "Orders", "Failed to load orders.", and "Try again" are hardcoded, while other components in the codebase use next-intl for translations. This creates inconsistency for non-English users.
Consider using useTranslations for these strings:
🌐 Proposed fix for i18n
+'use client';
+
+import { useTranslations } from 'next-intl';
import { cn } from '@/lib/utils';
// ... other imports
export default function OrdersError({ reset }: ErrorPageProps) {
+ const t = useTranslations('shop.orders');
+
return (
<div className="mx-auto w-full max-w-3xl px-4 py-8">
<header className="flex items-start justify-between gap-3">
- <h1 className="text-2xl font-semibold">Orders</h1>
+ <h1 className="text-2xl font-semibold">{t('title')}</h1>
</header>
<section className="mt-6 rounded-lg border border-border bg-card p-5">
- <p className="text-sm text-muted-foreground">Failed to load orders.</p>
+ <p className="text-sm text-muted-foreground">{t('loadError')}</p>
<div className="mt-4 flex flex-wrap items-center gap-3">
<button type="button" className={SHOP_OUTLINE_BTN} onClick={reset}>
- Try again
+ {t('tryAgain')}
</button>🤖 Prompt for AI Agents
In `@frontend/app/`[locale]/shop/orders/error.tsx around lines 25 - 34, Replace
the hardcoded English strings with next-intl translations: import
useTranslations from 'next-intl' in the component, call const t =
useTranslations('orders') (or appropriate namespace), then replace "Orders" with
t('title'), "Failed to load orders." with t('loadError') and "Try again" with
t('tryAgain'); keep the existing SHOP_OUTLINE_BTN class and the reset onClick
handler unchanged so only the displayed strings are swapped to t(...) keys.
| export const metadata: Metadata = { | ||
| title: 'Product name | DevLovers', | ||
| description: 'Details, price, and availability for product.', | ||
| }; |
There was a problem hiding this comment.
**Static metadata shows placeholder text instead of actual product name.**This is a dynamic product page ([slug]), but the metadata uses a static placeholder title "Product name | DevLovers". "Dynamic metadata depends on dynamic information, such as the current route parameters, external data, or metadata in parent segments, can be set by exporting a generateMetadata function that returns a Metadata object."
For proper SEO, use generateMetadata to fetch the actual product name and description.
Proposed fix using generateMetadata
-export const metadata: Metadata = {
- title: 'Product name | DevLovers',
- description: 'Details, price, and availability for product.',
-};
+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 {
+ title: 'Product Not Found | DevLovers',
+ description: 'The requested product could not be found.',
+ };
+ }
+
+ return {
+ title: `${product.name} | DevLovers`,
+ description: product.description ?? `Details, price, and availability for ${product.name}.`,
+ };
+}📝 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.
| export const metadata: Metadata = { | |
| title: 'Product name | DevLovers', | |
| description: 'Details, price, and availability for product.', | |
| }; | |
| 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 { | |
| title: 'Product Not Found | DevLovers', | |
| description: 'The requested product could not be found.', | |
| }; | |
| } | |
| return { | |
| title: `${product.name} | DevLovers`, | |
| description: product.description ?? `Details, price, and availability for ${product.name}.`, | |
| }; | |
| } |
🤖 Prompt for AI Agents
In `@frontend/app/`[locale]/shop/products/[slug]/page.tsx around lines 14 - 17,
The file currently exports a static metadata const with placeholder text;
replace it with an exported async generateMetadata function that receives the
route params (params.slug), fetches the product data for that slug, and returns
a Metadata object containing the real title and description (e.g.,
`${product.name} | DevLovers` and product.description). Remove or stop exporting
the static export const metadata and ensure generateMetadata handles
errors/missing product by returning a sensible fallback metadata. Use the symbol
generateMetadata and reference params.slug and the existing product-fetching
utility you have in the codebase to implement this.
| const wrapWithBadge = (node: React.ReactNode) => { | ||
| if (!badge) return node; | ||
|
|
There was a problem hiding this comment.
Don’t treat numeric/empty-string badges as “absent”.
if (!badge) will suppress valid badges like 0 or ''. Use a nullish check instead.
Suggested fix
- if (!badge) return node;
+ if (badge == null) return node;📝 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 wrapWithBadge = (node: React.ReactNode) => { | |
| if (!badge) return node; | |
| const wrapWithBadge = (node: React.ReactNode) => { | |
| if (badge == null) return node; | |
🤖 Prompt for AI Agents
In `@frontend/components/shared/HeaderButton.tsx` around lines 134 - 136, In
wrapWithBadge, the current guard uses a falsy check (if (!badge)) which
incorrectly treats valid badges like 0 or '' as absent; change this to a nullish
check (e.g., if (badge == null)) so only null/undefined skip rendering the
badge, keeping valid numeric/empty-string badges visible when wrapping the node.
| <div className="relative w-full sm:w-52"> | ||
| <select | ||
| id={selectId} | ||
| name="sort" | ||
| value={currentSort} | ||
| onChange={e => handleSort(e.target.value)} | ||
| className={cn( | ||
| SHOP_SELECT_BASE, | ||
| SHOP_SELECT_INTERACTIVE, | ||
| SHOP_FOCUS, | ||
| SHOP_DISABLED, | ||
| isActive ? 'text-foreground' : 'text-muted-foreground' | ||
| )} | ||
| > | ||
| {SORT_OPTIONS.map(option => ( | ||
| <option key={option.value} value={option.value}> | ||
| {getOptionLabel(option.value)} | ||
| </option> | ||
| ))} | ||
| </select> | ||
|
|
||
| <ChevronDown | ||
| className={cn( | ||
| 'pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2', | ||
| 'text-muted-foreground transition-colors', | ||
| 'peer-hover:text-foreground peer-focus-visible:text-foreground' | ||
| )} | ||
| aria-hidden="true" | ||
| /> | ||
| </div> |
There was a problem hiding this comment.
Missing peer class on select element breaks icon interaction states.
The ChevronDown icon uses peer-hover:text-foreground and peer-focus-visible:text-foreground classes (lines 98-99), but the <select> element is missing the peer class. This means the icon color won't change on hover or focus as intended.
Proposed fix
<select
id={selectId}
name="sort"
value={currentSort}
onChange={e => handleSort(e.target.value)}
className={cn(
+ 'peer',
SHOP_SELECT_BASE,
SHOP_SELECT_INTERACTIVE,
SHOP_FOCUS,
SHOP_DISABLED,
isActive ? 'text-foreground' : 'text-muted-foreground'
)}
>📝 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.
| <div className="relative w-full sm:w-52"> | |
| <select | |
| id={selectId} | |
| name="sort" | |
| value={currentSort} | |
| onChange={e => handleSort(e.target.value)} | |
| className={cn( | |
| SHOP_SELECT_BASE, | |
| SHOP_SELECT_INTERACTIVE, | |
| SHOP_FOCUS, | |
| SHOP_DISABLED, | |
| isActive ? 'text-foreground' : 'text-muted-foreground' | |
| )} | |
| > | |
| {SORT_OPTIONS.map(option => ( | |
| <option key={option.value} value={option.value}> | |
| {getOptionLabel(option.value)} | |
| </option> | |
| ))} | |
| </select> | |
| <ChevronDown | |
| className={cn( | |
| 'pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2', | |
| 'text-muted-foreground transition-colors', | |
| 'peer-hover:text-foreground peer-focus-visible:text-foreground' | |
| )} | |
| aria-hidden="true" | |
| /> | |
| </div> | |
| <div className="relative w-full sm:w-52"> | |
| <select | |
| id={selectId} | |
| name="sort" | |
| value={currentSort} | |
| onChange={e => handleSort(e.target.value)} | |
| className={cn( | |
| 'peer', | |
| SHOP_SELECT_BASE, | |
| SHOP_SELECT_INTERACTIVE, | |
| SHOP_FOCUS, | |
| SHOP_DISABLED, | |
| isActive ? 'text-foreground' : 'text-muted-foreground' | |
| )} | |
| > | |
| {SORT_OPTIONS.map(option => ( | |
| <option key={option.value} value={option.value}> | |
| {getOptionLabel(option.value)} | |
| </option> | |
| ))} | |
| </select> | |
| <ChevronDown | |
| className={cn( | |
| 'pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2', | |
| 'text-muted-foreground transition-colors', | |
| 'peer-hover:text-foreground peer-focus-visible:text-foreground' | |
| )} | |
| aria-hidden="true" | |
| /> | |
| </div> |
🤖 Prompt for AI Agents
In `@frontend/components/shop/product-sort.tsx` around lines 73 - 102, The select
element used to render sorting (the <select> with id={selectId}, name="sort",
value={currentSort} and onChange calling handleSort) lacks the "peer" utility
class so the ChevronDown icon's peer-hover and peer-focus-visible selectors
never trigger; add the "peer" class to the select's className (alongside
SHOP_SELECT_BASE, SHOP_SELECT_INTERACTIVE, SHOP_FOCUS, SHOP_DISABLED and the
conditional isActive text class) so the ChevronDown (component ChevronDown)
responds to hover/focus as intended.
| export async function getQaCache<T>(key: string) { | ||
| const redis = getRedisClient(); | ||
| if (!redis) return null; | ||
|
|
||
| const cached = await redis.get<T | string>(key); | ||
| if (!cached) return null; | ||
|
|
||
| if (typeof cached !== 'string') { | ||
| return cached as T; | ||
| } | ||
|
|
||
| try { | ||
| return JSON.parse(cached) as T; | ||
| } catch (error) { | ||
| console.warn('[qa-cache] Failed to parse cached value', error); | ||
| await redis.del(key); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| export async function setQaCache<T>(key: string, value: T) { | ||
| const redis = getRedisClient(); | ||
| if (!redis) return; | ||
|
|
||
| await redis.set(key, value, { ex: QA_CACHE_TTL_SECONDS }); | ||
| } | ||
|
|
||
| export async function invalidateQaCacheByCategory(category: string) { | ||
| const redis = getRedisClient(); | ||
| if (!redis) return 0; | ||
|
|
||
| const prefix = `qa:category:${category.toLowerCase()}:`; | ||
| let cursor = 0; | ||
| let deleted = 0; | ||
|
|
||
| do { | ||
| const [nextCursor, keys] = await redis.scan(cursor, { | ||
| match: `${prefix}*`, | ||
| count: 100, | ||
| }); | ||
| cursor = Number(nextCursor); | ||
|
|
||
| if (keys.length) { | ||
| const removed = await redis.del(...keys); | ||
| deleted += removed ?? 0; | ||
| } | ||
| } while (cursor !== 0); | ||
|
|
||
| return deleted; | ||
| } | ||
|
|
||
| export async function invalidateAllQaCache() { | ||
| const redis = getRedisClient(); | ||
| if (!redis) return 0; | ||
|
|
||
| let cursor = 0; | ||
| let deleted = 0; | ||
|
|
||
| do { | ||
| const [nextCursor, keys] = await redis.scan(cursor, { | ||
| match: 'qa:*', | ||
| count: 200, | ||
| }); | ||
| cursor = Number(nextCursor); | ||
|
|
||
| if (keys.length) { | ||
| const removed = await redis.del(...keys); | ||
| deleted += removed ?? 0; | ||
| } | ||
| } while (cursor !== 0); | ||
|
|
||
| return deleted; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd "qa.ts" -t fRepository: DevLoversTeam/devlovers.net
Length of output: 95
🌐 Web query:
@upstash/redis JavaScript client error handling - do get/set/scan/del throw on network/auth errors
💡 Result:
With the current @upstash/redis SDK (v1+), the instance methods like redis.get, redis.set, redis.scan, redis.del, etc. return only the successful result and throw when something goes wrong (instead of returning { data, error } like the pre-v1 client). [1]
What gets thrown:
- Auth / Redis-level errors (e.g. wrong or missing token) are thrown as an
UpstashErrorsuch asUpstashError: WRONGPASS invalid or missing auth token. [2] - Network / transport errors: the SDK automatically retries on network errors (default 5 retries with exponential backoff). If it still can’t complete, the request ultimately fails (throws) after retries are exhausted. [3]
So yes: get/set/scan/del will throw on network and auth errors in v1+. [1][2][3]
If you’re on the old pre-v1 API (the one where you import command functions like set(...)), that older client returned { data, error } and you had to manually check/throw, as shown in the v0.2.1 vs v1 example. [1]
References:
[1] https://upstash.com/blog/upstash-redis-sdk-v1
[2] https://upstash.com/docs/redis/troubleshooting/http_unauthorized
[3] https://upstash.com/docs/redis/sdks/ts/retries
🏁 Script executed:
git ls-files | grep -E "(cache|redis)" | head -20Repository: DevLoversTeam/devlovers.net
Length of output: 179
🏁 Script executed:
cat frontend/lib/cache/qa.tsRepository: DevLoversTeam/devlovers.net
Length of output: 2365
🏁 Script executed:
cat frontend/lib/redis.tsRepository: DevLoversTeam/devlovers.net
Length of output: 559
🏁 Script executed:
rg "getQaCache|setQaCache|invalidateQaCache" --type ts --type tsx -A 2 -B 2 | head -100Repository: DevLoversTeam/devlovers.net
Length of output: 98
🏁 Script executed:
rg "getQaCache|setQaCache|invalidateQaCache" --type ts -A 2 -B 2 | head -100Repository: DevLoversTeam/devlovers.net
Length of output: 1970
🏁 Script executed:
cat frontend/app/api/questions/[category]/route.ts | head -150Repository: DevLoversTeam/devlovers.net
Length of output: 3857
Wrap Redis operations with try/catch to ensure best-effort caching behavior.
The @upstash/redis v1+ client throws on network and auth errors rather than returning gracefully. Currently, getQaCache(), setQaCache(), invalidateQaCacheByCategory(), and invalidateAllQaCache() have no error handling around Redis operations. A Redis outage will cause these functions to throw and fail the entire request instead of gracefully degrading (cache miss) or silently failing (cache write).
Wrap each Redis operation with try/catch at the function level and return safe fallbacks—null for gets, silent failure for sets/deletes. This keeps caching best-effort and prevents Redis from cascading failures across the API.
Suggested implementation
export async function getQaCache<T>(key: string) {
const redis = getRedisClient();
if (!redis) return null;
+ try {
const cached = await redis.get<T | string>(key);
if (!cached) return null;
if (typeof cached !== 'string') {
return cached as T;
}
try {
return JSON.parse(cached) as T;
} catch (error) {
console.warn('[qa-cache] Failed to parse cached value', error);
await redis.del(key);
return null;
}
+ } catch (error) {
+ console.warn('[qa-cache] Redis get failed', error);
+ return null;
+ }
}
export async function setQaCache<T>(key: string, value: T) {
const redis = getRedisClient();
if (!redis) return;
+ try {
await redis.set(key, value, { ex: QA_CACHE_TTL_SECONDS });
+ } catch (error) {
+ console.warn('[qa-cache] Redis set failed', error);
+ }
}
export async function invalidateQaCacheByCategory(category: string) {
const redis = getRedisClient();
if (!redis) return 0;
const prefix = `qa:category:${category.toLowerCase()}:`;
let cursor = 0;
let deleted = 0;
+ try {
do {
const [nextCursor, keys] = await redis.scan(cursor, {
match: `${prefix}*`,
count: 100,
});
cursor = Number(nextCursor);
if (keys.length) {
const removed = await redis.del(...keys);
deleted += removed ?? 0;
}
} while (cursor !== 0);
+ } catch (error) {
+ console.warn('[qa-cache] Redis invalidation failed', error);
+ }
return deleted;
}
export async function invalidateAllQaCache() {
const redis = getRedisClient();
if (!redis) return 0;
let cursor = 0;
let deleted = 0;
+ try {
do {
const [nextCursor, keys] = await redis.scan(cursor, {
match: 'qa:*',
count: 200,
});
cursor = Number(nextCursor);
if (keys.length) {
const removed = await redis.del(...keys);
deleted += removed ?? 0;
}
} while (cursor !== 0);
+ } catch (error) {
+ console.warn('[qa-cache] Redis invalidation failed', error);
+ }
return deleted;
}🤖 Prompt for AI Agents
In `@frontend/lib/cache/qa.ts` around lines 27 - 99, All Redis calls in
getQaCache, setQaCache, invalidateQaCacheByCategory, and invalidateAllQaCache
must be wrapped in try/catch so client errors don't bubble up; update each
function to wrap the Redis operations (you can wrap the whole function body
after retrieving getRedisClient()) and on error return safe fallbacks:
getQaCache -> null, setQaCache -> silently return (no throw),
invalidateQaCacheByCategory/invalidateAllQaCache -> return 0; include the error
in a console.warn/log statement for observability but do not rethrow.
| { | ||
| "name": "frontend", | ||
| "version": "0.5.0", | ||
| "version": "0.5.1", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat frontend/package.jsonRepository: DevLoversTeam/devlovers.net
Length of output: 2848
🏁 Script executed:
npm view `@upstash/redis`@1.36.1 engines peerDependencies
npm view `@upstash/redis`@1.36.1 keywords
npm view `@netlify/plugin-nextjs`@5.15.7 engines peerDependencies
npm view `@netlify/plugin-nextjs`@5.15.7 keywords
npm audit --json | jq '.vulnerabilities | length'Repository: DevLoversTeam/devlovers.net
Length of output: 567
🏁 Script executed:
npm view `@netlify/plugin-nextjs`@5.15.7 peerDependencies engines
npm info `@netlify/plugin-nextjs`@5.15.7 | grep -A 20 "peerDependencies\|engines"Repository: DevLoversTeam/devlovers.net
Length of output: 91
🌐 Web query:
@netlify/plugin-nextjs 5.15.7 Next.js 16 compatibility
💡 Result:
- Netlify supports Next.js 16 “with zero configuration” via its OpenNext adapter; upgrading from Next.js 15 to 16 requires no Netlify config changes unless you opted out of automatic OpenNext adapter updates. [1]
- Netlify’s docs state they verify compatibility with every new stable Next.js release and recommend not pinning the adapter/runtime version. [2]
- If you have
@netlify/plugin-nextjspinned (e.g.,5.15.7), Netlify treats that as opting into the legacy plugin system; their upgrade guidance is to remove@netlify/plugin-nextjsfrompackage.jsonandnetlify.tomlso the latest adapter is applied automatically (best path for Next.js 16). [3] - Baseline requirements for the current runtime/adapter line are Next.js ≥ 13.5 and Node.js ≥ 18. [4]
Recommendation for Next.js 16: remove the pinned @netlify/plugin-nextjs and let Netlify apply the latest OpenNext adapter automatically; only pin if you have a specific reason and are prepared to update it as Next.js releases evolve. [1][3]
Sources
[1] Netlify Changelog: “Next.js 16 is ready to deploy on Netlify” https://www.netlify.com/changelog/next-js-16-deploy-on-netlify/
[2] Netlify Docs (Next.js overview / compatibility & testing) https://docs.netlify.com/frameworks/next-js/overview/
[3] Netlify Docs (Upgrading or reverting the Next.js adapter) https://docs.netlify.com/build/frameworks/framework-setup-guides/nextjs/legacy-runtime/upgrading-or-reverting-adapter/
[4] OpenNext Netlify adapter repo (prerequisites) https://github.com/opennextjs/opennextjs-netlify
Consider removing pinned @netlify/plugin-nextjs for Next.js 16 compatibility.
Engine requirements are satisfied (Node ≥18 for both dependencies; project uses ^20.19.30), and no security advisories are present. However, pinning @netlify/plugin-nextjs@5.15.7 is Netlify's legacy plugin system. For Next.js 16, Netlify recommends removing this pinned dependency from package.json and netlify.toml to enable automatic OpenNext adapter updates, which are verified for each Next.js release and provide better long-term compatibility.
🤖 Prompt for AI Agents
In `@frontend/package.json` at line 3, Remove the pinned Netlify plugin entry for
`@netlify/plugin-nextjs`@5.15.7 to allow Netlify's OpenNext adapter to manage the
correct adapter for Next.js 16: edit frontend/package.json to delete the
explicit `@netlify/plugin-nextjs` dependency (and any version lock) and also
remove corresponding configuration blocks in netlify.toml that reference that
legacy plugin so Netlify can auto-select the OpenNext adapter; ensure no other
code assumes the legacy plugin name.
| @@ -1 +1,4 @@ | |||
| ## DevLovers - a platform for technical interview preparation in frontend, backend, and full-stack development. | |||
|
|
|||
| develop | |||
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's see the current state of README.md
head -20 README.mdRepository: DevLoversTeam/devlovers.net
Length of output: 361
🏁 Script executed:
# Let's also check the git diff to see what was changed
git diff HEAD~1 README.md || git diff README.mdRepository: DevLoversTeam/devlovers.net
Length of output: 257
Clarify or remove the standalone "develop" text.
The word "develop" on line 3 appears without formatting or context. While it likely labels the Netlify deployment status badge below, its presentation is unclear. Consider formatting it as a proper markdown heading:
-develop
+## Deployment Status (Develop)Or remove it and rely on the badge itself to convey the deployment information.
📝 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.
| develop | |
| ## Deployment Status (Develop) |
🤖 Prompt for AI Agents
In `@README.md` at line 3, The README contains a stray unformatted token "develop"
that is unclear; either remove that standalone word or convert it into a proper
Markdown heading (e.g., change "develop" to "## develop" or another appropriate
heading text) so it clearly labels the Netlify deployment badge or add
surrounding context text explaining what "develop" refers to; update the
README.md content accordingly.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@frontend/app/`[locale]/blog/[slug]/PostDetails.tsx:
- Line 86: Remove the unused declaration "const linkKey = marks.find(mark =>
linkMap.has(mark));" from PostDetails.tsx since the subsequent loop directly
uses linkMap.has(mark) and never reads linkKey; locate the declaration near the
code that references marks and linkMap in the PostDetails component and delete
it to avoid the unused-variable warning.
🧹 Nitpick comments (1)
frontend/app/[locale]/blog/[slug]/PostDetails.tsx (1)
235-246: UseImagefromnext/imagefor inline images.This segment uses a native
<img>tag while the rest of the file uses theImagecomponent fromnext/image(lines 528, 569, 591). Using the native tag bypasses Next.js image optimization (lazy loading, format conversion, responsive sizing).♻️ Proposed fix
if (block?._type === 'image' && block?.url) { nodes.push( - <img + <Image key={block._key || `image-${i}`} src={block.url} - alt={postTitle || 'Post image'} - className="rounded-xl border border-gray-200 my-6" + alt={block.alt || postTitle || 'Post image'} + width={800} + height={450} + className="rounded-xl border border-gray-200 my-6 w-full h-auto" /> );Note: You may need to update the GROQ query to fetch
altfrom the image asset if available in your Sanity schema, or usefillwith a sized container if dimensions are unknown.
| @@ -85,25 +85,172 @@ function renderPortableTextSpans( | |||
| const marks = child?.marks || []; | |||
| const linkKey = marks.find(mark => linkMap.has(mark)); | |||
There was a problem hiding this comment.
Unused variable linkKey.
The variable linkKey is declared but never used. The loop below directly checks linkMap.has(mark) instead.
🧹 Proposed fix
const marks = child?.marks || [];
- const linkKey = marks.find(mark => linkMap.has(mark));
let node: React.ReactNode =📝 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 linkKey = marks.find(mark => linkMap.has(mark)); | |
| const marks = child?.marks || []; | |
| let node: React.ReactNode = |
🤖 Prompt for AI Agents
In `@frontend/app/`[locale]/blog/[slug]/PostDetails.tsx at line 86, Remove the
unused declaration "const linkKey = marks.find(mark => linkMap.has(mark));" from
PostDetails.tsx since the subsequent loop directly uses linkMap.has(mark) and
never reads linkKey; locate the declaration near the code that references marks
and linkMap in the PostDetails component and delete it to avoid the
unused-variable warning.
* chore: bump Node.js to 20 for Netlify * feat(md) add netlify status (#234) * (SP 2) [Shop UI] Unify storefront styles across components and interactions (#236) * Host (#237) * feat(md) add netlify status * feat(files): add packages * fix(auth): use currentTarget for email input validity * Host (#238) * feat(md) add netlify status * feat(files): add packages * fix(auth): use currentTarget for email input validity * fix(auth): use currentTarget for email input validity * fix(auth): use currentTarget in password field * (SP 1) [Shop UI] Add page metadata across shop routes (#239) * (SP 2) [Shop UI] Unify storefront styles across components and interactions * (SP 1) [Shop UI] Add page metadata across shop routes * (SP: 3) [Cache] Add Upstash Redis cache for Q&A (#241) * 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 Q&A Redis cache parsing for Upstash REST (#243) * 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):Adding pagination (#244) * 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 * (SP:2) feat(api): clean up AI helper for Vercel & fix orders i18n (#245) - 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 * refactor(home): update button, cards, and online counter UI (#248) Improve primary button styles Fix card overflow and add subtle highlights Redesign online users counter and reduce font size * fix(api): enforce rate limiting (#246) * (SP:2) feat(api): clean up AI helper for Vercel & fix orders i18n - 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 * fix(api): enforce rate limiting --------- Co-authored-by: Viktor Svertoka <victor.svertoka@gmail.com> * feat(Blog):formating text (#249) * 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 * feat(Blog):adding text formating fix * ref(files): refactoring code & bag fix (#250) * chore(release): v0.5.2 --------- Co-authored-by: liudmylasovetovs <127711697+liudmylasovetovs@users.noreply.github.com> Co-authored-by: KomrakovaAnna <komrakova.anna@gmail.com> Co-authored-by: Tetiana Zorii <131365289+TiZorii@users.noreply.github.com> Co-authored-by: Yuliia Nazymko <122815071+YNazymko12@users.noreply.github.com>
* chore: bump Node.js to 20 for Netlify * feat(md) add netlify status (#234) * (SP 2) [Shop UI] Unify storefront styles across components and interactions (#236) * Host (#237) * feat(md) add netlify status * feat(files): add packages * fix(auth): use currentTarget for email input validity * Host (#238) * feat(md) add netlify status * feat(files): add packages * fix(auth): use currentTarget for email input validity * fix(auth): use currentTarget for email input validity * fix(auth): use currentTarget in password field * (SP 1) [Shop UI] Add page metadata across shop routes (#239) * (SP 2) [Shop UI] Unify storefront styles across components and interactions * (SP 1) [Shop UI] Add page metadata across shop routes * (SP: 3) [Cache] Add Upstash Redis cache for Q&A (#241) * 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 Q&A Redis cache parsing for Upstash REST (#243) * 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):Adding pagination (#244) * 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 * (SP:2) feat(api): clean up AI helper for Vercel & fix orders i18n (#245) - 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 * refactor(home): update button, cards, and online counter UI (#248) Improve primary button styles Fix card overflow and add subtle highlights Redesign online users counter and reduce font size * fix(api): enforce rate limiting (#246) * (SP:2) feat(api): clean up AI helper for Vercel & fix orders i18n - 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 * fix(api): enforce rate limiting --------- Co-authored-by: Viktor Svertoka <victor.svertoka@gmail.com> * feat(Blog):formating text (#249) * 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 * feat(Blog):adding text formating fix * ref(files): refactoring code & bag fix (#250) * chore(release): v0.5.2 * Lso/feat/shop design (#252) * feat(i18n): add translations for blog categories, and UI components (#253) - Translate blog category labels in mobile menu, cards, and filters - Add CTA hover variants translations - Add aria-label translations for theme toggle, cart, search, GitHub star - Update translation files for EN, UK, PL locales * feat blog: fix for paddings on mobile (#254) * 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 * feat(Blog):adding text formating fix * chore(lint): finalize ESLint + Prettier (#256) * feat(leaderboard): finalize components and fix lint errors (#259) * (SP 1) [FIX] set up eslint/prettier + stabilize formatting workflow (own files only) (#264) * (SP 2) [Shop UI] Unify storefront styles across components and interactions * (SP 1) [Shop UI] Add page metadata across shop routes * (SP 1) [FIX] names of components, replacing tests, clean code * (SP 1) [FIX] remove magic constant and align restock sweep test typing * (SP 1) [FIX] remove duplicate * (SP 1) [FIX] set up eslint/prettier + stabilize formatting workflow (own files only) * (SP 1) [FIX] Tailwind hints * (SP 1) [FIX] Tailwind hints revert * (SP: 5) [Quiz] Redis caching + guest session fix + cleanup (#263) * feat(quiz-ui): quiz UI polish - tabs, category accents, color scheme (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 * docs: update .gitignore * fix(quiz): align disqualification threshold with warning banner Changed violationsCount > 3 to >= 3 in QuizResult points block to match the warning banner threshold at line 124. * feat(quiz-testing): add quiz unit tests - Configure Vitest for quiz module - Add test factories and setup utilities - Add quiz-crypto tests (13 tests) - Add quiz-session tests (12 tests) * test(quiz): add integration tests for verify-answer API and useAntiCheat 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) * test(quiz): expand test coverage to 90%+ with hooks, API routes, and 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 * chore: remove coverage-quiz from git, add to .gitignore * chore: add coverage-quiz to .gitignore, fix quiz guards test * fix(a11y): improve quiz accessibility and i18n compliance * fix(sl/feat/quiz): replace with correct name for react icon * feat(quiz): implement Redis caching + session fixes + cleanup Closes #260, #261, #262 - Add quiz-answers-redis.ts with getOrCreateQuizAnswersCache() - Cache correct answers per quiz (12h TTL) - Replace AES-256-GCM decryption with O(1) Redis lookup - Add initializeQuizCache server action - Update verify-answer route to use Redis - Allow restoring 'completed' sessions (not just 'in_progress') - Only clear session for authenticated users after submit - Guest result screen now survives language switch - Delete PendingResultHandler.tsx (never executes) - Delete start-session/route.ts (broken import, unused) - Delete quiz-crypto.ts (AES replaced by Redis) - Delete quiz-crypto.test.ts (tests dead code) - Rewrite verify-answer.test.ts for Redis API (8 tests) - Fix quiz-session.test.ts for completed session restore * chore(quiz): delete unused start-session route Part of #262 cleanup - route had broken import after Redis migration. * git commit -m "fix(quiz): add NaN seed validation and cache/DB fallback - Validate seed param to prevent NaN breaking question shuffle - Add cache recovery in verify-answer when Redis cache expires - Add DB fallback in getCorrectAnswer when Redis unavailable * chore: remove redis ttl for static quiz and qa caches (#265) * fix(layout): remove duplicate padding from quiz routes (#266) * feat(quiz-ui): quiz UI polish - tabs, category accents, color scheme (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 * docs: update .gitignore * fix(quiz): align disqualification threshold with warning banner Changed violationsCount > 3 to >= 3 in QuizResult points block to match the warning banner threshold at line 124. * feat(quiz-testing): add quiz unit tests - Configure Vitest for quiz module - Add test factories and setup utilities - Add quiz-crypto tests (13 tests) - Add quiz-session tests (12 tests) * test(quiz): add integration tests for verify-answer API and useAntiCheat 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) * test(quiz): expand test coverage to 90%+ with hooks, API routes, and 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 * chore: remove coverage-quiz from git, add to .gitignore * chore: add coverage-quiz to .gitignore, fix quiz guards test * fix(a11y): improve quiz accessibility and i18n compliance * fix(sl/feat/quiz): replace with correct name for react icon * feat(quiz): implement Redis caching + session fixes + cleanup Closes #260, #261, #262 - Add quiz-answers-redis.ts with getOrCreateQuizAnswersCache() - Cache correct answers per quiz (12h TTL) - Replace AES-256-GCM decryption with O(1) Redis lookup - Add initializeQuizCache server action - Update verify-answer route to use Redis - Allow restoring 'completed' sessions (not just 'in_progress') - Only clear session for authenticated users after submit - Guest result screen now survives language switch - Delete PendingResultHandler.tsx (never executes) - Delete start-session/route.ts (broken import, unused) - Delete quiz-crypto.ts (AES replaced by Redis) - Delete quiz-crypto.test.ts (tests dead code) - Rewrite verify-answer.test.ts for Redis API (8 tests) - Fix quiz-session.test.ts for completed session restore * chore(quiz): delete unused start-session route Part of #262 cleanup - route had broken import after Redis migration. * git commit -m "fix(quiz): add NaN seed validation and cache/DB fallback - Validate seed param to prevent NaN breaking question shuffle - Add cache recovery in verify-answer when Redis cache expires - Add DB fallback in getCorrectAnswer when Redis unavailable * fix(layout): remove duplicate padding from quiz routes Add isQuizzesPath to MainSwitcher to exclude /quiz and /quizzes routes from extra px-6 padding, matching Q&A page behavior. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: liudmylasovetovs <127711697+liudmylasovetovs@users.noreply.github.com> Co-authored-by: KomrakovaAnna <komrakova.anna@gmail.com> Co-authored-by: Tetiana Zorii <131365289+TiZorii@users.noreply.github.com> Co-authored-by: Yuliia Nazymko <122815071+YNazymko12@users.noreply.github.com> Co-authored-by: AlinaRyabova <115992255+AlinaRyabova@users.noreply.github.com> Co-authored-by: Lesia Soloviova <106915140+LesiaUKR@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adding pagination to blog page
Summary by CodeRabbit
New Features
Improvements
Chores
✏️ Tip: You can customize this high-level summary in your review settings.