Skip to content

feat(Blog):Adding pagination#244

Merged
ViktorSvertoka merged 32 commits into
developfrom
sanity
Feb 1, 2026
Merged

feat(Blog):Adding pagination#244
ViktorSvertoka merged 32 commits into
developfrom
sanity

Conversation

@KomrakovaAnna
Copy link
Copy Markdown
Collaborator

@KomrakovaAnna KomrakovaAnna commented Feb 1, 2026

Adding pagination to blog page

Summary by CodeRabbit

  • New Features

    • Improved shopping cart UI with client-side checkout flow and localized currency/stock handling
    • Blog pagination and paginated navigation component
    • Unified, reusable CTA and outline buttons with animated gradient visuals
  • Improvements

    • Page metadata added across shop pages for better SEO
    • Visual/styling updates across shop, product, and admin views (consistent CTAs, badges, and link styles)
    • Minor form validation robustness and accessibility fixes
  • Chores

    • Redis-backed QA caching and related environment vars
    • Dependency and deployment config updates (Netlify)

✏️ Tip: You can customize this high-level summary in your review settings.

….org JSON‑LD for Article (BlogPosting) and BreadcrumbList , Added <time datetime> tags where blog dates renders
…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
feat(Blog): merging develop
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Feb 1, 2026

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

Project Deployment Actions Updated (UTC)
devlovers-net Ready Ready Preview, Comment Feb 1, 2026 2:42pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 1, 2026

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Redis & QA cache
frontend/lib/redis.ts, frontend/lib/cache/qa.ts, frontend/app/api/questions/[category]/route.ts
Add Upstash REST Redis client factory, QA cache helpers (get/set/invalidate, TTL), and integrate cache-first logic into questions API route.
Env, deps, and Netlify
frontend/.env.example, frontend/lib/env/index.ts, frontend/package.json, studio/package.json, netlify.toml, README.md
Add UPSTASH env vars to example and schema, bump frontend/studio versions, add @upstash/redis dep, Netlify build config, and README Netlify badge.
Shop UI tokens & globals
frontend/lib/shop/ui-classes.ts, frontend/app/globals.css
Introduce centralized SHOP_* UI class constants and shopCtaGradient; add many CSS variables/tokens for light/dark shop theming.
Shop components — CTA / styling migration
frontend/components/shop/*, frontend/app/[locale]/shop/* (cart, checkout, success, products, admin pages, product-card, add-to-cart, hero, filters, sort, catalog-load-more)
Replace many inline Tailwind classes with SHOP_* tokens, add layered CTA/outline button patterns, consolidate link styles, and add page metadata exports on multiple shop routes.
Cart refactor
frontend/app/[locale]/shop/cart/CartPageClient.tsx, frontend/app/[locale]/shop/cart/page.tsx
Introduce new client-side CartPageClient component housing cart state and checkout flow; page.tsx now a thin wrapper and adds static metadata.
Blog pagination & portable text
frontend/components/blog/BlogFilters.tsx, frontend/components/blog/BlogPagination.tsx, frontend/app/[locale]/blog/[slug]/PostDetails.tsx
Add client-side pagination with URL-sync in BlogFilters and a new BlogPagination component; refactor PostDetails to a portable-text renderer for richer content/marks and images.
Translations
frontend/messages/en.json, frontend/messages/pl.json, frontend/messages/uk.json
Add blog.pagination translations (label, previous/next, page placeholders).
Shared components & header/footer
frontend/components/shared/HeaderButton.tsx, frontend/components/shared/Footer.tsx, frontend/components/theme/ThemeToggle.tsx
Add badge props/wrapping to HeaderButton, footer theming using CSS vars and shop scope, and theme toggle hover variable fallback.
Auth fields minor fix
frontend/components/auth/fields/EmailField.tsx, .../NameField.tsx, .../PasswordField.tsx
Use e.currentTarget instead of e.target in invalid/change handlers to reliably access input element.
Blog & admin minor UI changes
frontend/components/*, frontend/app/[locale]/shop/admin/*, frontend/components/shop/header/cart-button.tsx
Various presentational updates: full-width/wrapping buttons, cart badge capping, standardized link styles, and small accessibility/ARIA refinements.
Misc cleanup
frontend/app/[locale]/shop/checkout/success/OrderStatusAutoRefresh.tsx
Removed header comment; no behavior change.

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
Loading
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()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #230: Overlaps on Upstash Redis integration, QA cache implementation, and related exports.
  • PR #233: Adds the same Netlify build configuration and README Netlify badge.
  • PR #172: Also modifies the questions API route and caching/cache-control behavior.

Suggested labels

enhancement

Suggested reviewers

  • AM1007
  • ViktorSvertoka

Poem

🐰 I hopped through code with nimble feet,

Caching questions and pagination neat.
CTAs gleam and badges shine bright,
Cart moved client-side — everything's light!
Tea-time for rabbits, code wrapped tight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(Blog): Adding pagination' accurately summarizes the primary change: pagination functionality was added to the blog page, as evidenced by new BlogPagination component, pagination state management in BlogFilters, and related translation keys.

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

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

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

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 | 🟡 Minor

Potential SQL injection via search parameter.

The search variable is interpolated directly into the ilike pattern without escaping SQL wildcards (%, _). A malicious search string could manipulate the query pattern. While ilike with 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.

setQaCache is 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() (using clsx + twMerge) handles this gracefully, using these constants directly (without cn()) may produce unexpected whitespace in the DOM class attribute. 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: The error prop is declared but unused.

The ErrorPageProps type includes an error prop with an optional digest, 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 HeroProps interface 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 reusable ShopCta or HeroCtaInner component to reduce duplication and ensure consistent updates.

frontend/app/[locale]/shop/checkout/payment/StripePaymentClient.tsx (1)

102-143: Consider extracting HeroCtaInner to 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., alongside ui-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_LINK and ORDER_HEADLINE_LINK are 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 using any type for product.

Casting to any loses 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 checks product.badge but renders badgeLabel.

Line 69 conditionally renders based on product.badge, but the badge label is derived from the badge variable (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 !important modifiers in Tailwind classes.

The !underline !decoration-2 !underline-offset-4 classes suggest a specificity conflict with the imported SHOP_LINK_BASE or SHOP_LINK_MD tokens. Consider adjusting the base token definitions instead of using !important overrides.

Investigation suggestion

Check if SHOP_LINK_BASE or SHOP_LINK_MD sets no-underline or conflicting decoration styles. If so, either:

  1. Create a separate token for underlined links
  2. Adjust the base token to not set text-decoration
  3. Use a more specific composition that doesn't require !important
   const 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 extracting HeroCtaInner and CTA constants to a shared location.

SHOP_HERO_CTA_SM, SHOP_OUTLINE_BTN, and HeroCtaInner are defined in this file but appear to be duplicated in checkout/error/page.tsx as well. Extracting these to a shared module (e.g., alongside frontend/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: Avoid as any type casting; prefer proper type narrowing.

The repeated (order as any).totalAmountMinor and (order as any).currency casts suggest the return type of getOrderSummary may not include these fields. This bypasses TypeScript's type checking and could mask runtime errors.

Consider updating the getOrderSummary return 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_SM and SHOP_OUTLINE_BTN are defined identically here and in checkout/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 to currentPage in dependency array.

This effect syncs currentPage state with parsedPage from URL. However, including currentPage in the dependency array means the effect runs whenever either value changes. Since setCurrentPage inside the effect changes currentPage, 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 currentPage changes from other sources (like setCurrentPage in onPageChange).


460-464: Potential for effect loop when clamping page.

When currentPage > totalPages, this effect sets currentPage to totalPages and calls updatePageParam. If updatePageParam triggers a URL change that causes parsedPage to 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 <= totalPages guard, but consider consolidating page clamping logic into the filter-change effect (lines 348-375) to reduce effect interdependencies.

Comment thread frontend/.env.example
Comment on lines +11 to +13
# --- Upstash Redis (REST)
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
# --- 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.

Comment on lines +70 to +78
const translateColor = (color: string | null | undefined): string | null => {
if (!color) return null;
const colorSlug = color.toLowerCase();
try {
return tColors(colorSlug);
} catch {
return color;
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

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 calling t('key') (useful for optional messages). [2]
  • Throw on missing by customizing onError (e.g. throw when error.code === IntlErrorCode.MISSING_MESSAGE) and optionally customize the UI fallback with getMessageFallback. [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:


🏁 Script executed:

# Find i18n or translation configuration files
find . -type f \( -name "*i18n*" -o -name "*translation*" -o -name "*config*" \) | head -20

Repository: 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 -50

Repository: 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 -100

Repository: DevLoversTeam/devlovers.net

Length of output: 3578


🏁 Script executed:

cat -n frontend/i18n/config.ts

Repository: 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 -10

Repository: 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 -5

Repository: 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 -10

Repository: 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 -50

Repository: DevLoversTeam/devlovers.net

Length of output: 1684


🏁 Script executed:

# Check the locale-specific layout
cat -n frontend/app/[locale]/layout.tsx | head -50

Repository: 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/null

Repository: 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 tsx

Repository: 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 -10

Repository: 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 -100

Repository: DevLoversTeam/devlovers.net

Length of output: 179


🏁 Script executed:

# Search with correct syntax for next-intl error handling
rg "onError|getMessageFallback|IntlError" frontend --type ts

Repository: 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.

Comment on lines +24 to +28
export const metadata: Metadata = {
title: 'Order Confirmed| DevLovers',
description:
'Your order has been placed. You can track its status on the order page.',
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +25 to +34
<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>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hardcoded English strings 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.

Comment on lines +14 to +17
export const metadata: Metadata = {
title: 'Product name | DevLovers',
description: 'Details, price, and availability for product.',
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

**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.

Suggested change
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.

Comment on lines +134 to +136
const wrapWithBadge = (node: React.ReactNode) => {
if (!badge) return node;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +73 to +102
<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>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
<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.

Comment thread frontend/lib/cache/qa.ts
Comment on lines +27 to +99
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;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd "qa.ts" -t f

Repository: 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 UpstashError such as UpstashError: 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 -20

Repository: DevLoversTeam/devlovers.net

Length of output: 179


🏁 Script executed:

cat frontend/lib/cache/qa.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 2365


🏁 Script executed:

cat frontend/lib/redis.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 559


🏁 Script executed:

rg "getQaCache|setQaCache|invalidateQaCache" --type ts --type tsx -A 2 -B 2 | head -100

Repository: DevLoversTeam/devlovers.net

Length of output: 98


🏁 Script executed:

rg "getQaCache|setQaCache|invalidateQaCache" --type ts -A 2 -B 2 | head -100

Repository: DevLoversTeam/devlovers.net

Length of output: 1970


🏁 Script executed:

cat frontend/app/api/questions/[category]/route.ts | head -150

Repository: 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.

Comment thread frontend/package.json
{
"name": "frontend",
"version": "0.5.0",
"version": "0.5.1",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat frontend/package.json

Repository: 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-nextjs pinned (e.g., 5.15.7), Netlify treats that as opting into the legacy plugin system; their upgrade guidance is to remove @netlify/plugin-nextjs from package.json and netlify.toml so 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.

Comment thread README.md
@@ -1 +1,4 @@
## DevLovers - a platform for technical interview preparation in frontend, backend, and full-stack development.

develop
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's see the current state of README.md
head -20 README.md

Repository: 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.md

Repository: 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.

Suggested change
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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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: Use Image from next/image for inline images.

This segment uses a native <img> tag while the rest of the file uses the Image component from next/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 alt from the image asset if available in your Sanity schema, or use fill with 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));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

@ViktorSvertoka ViktorSvertoka changed the base branch from main to develop February 1, 2026 14:55
@ViktorSvertoka ViktorSvertoka merged commit c23f2c0 into develop Feb 1, 2026
6 checks passed
@ViktorSvertoka ViktorSvertoka deleted the sanity branch February 1, 2026 14:56
ViktorSvertoka added a commit that referenced this pull request Feb 1, 2026
* 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>
ViktorSvertoka added a commit that referenced this pull request Feb 3, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants