Skip to content

(SP: 2) [Frontend] Quizzes page ISR + client-side progress + GitHub stars cache#371

Merged
ViktorSvertoka merged 4 commits intodevelopfrom
sl/feat/quiz
Feb 26, 2026
Merged

(SP: 2) [Frontend] Quizzes page ISR + client-side progress + GitHub stars cache#371
ViktorSvertoka merged 4 commits intodevelopfrom
sl/feat/quiz

Conversation

@LesiaUKR
Copy link
Collaborator

@LesiaUKR LesiaUKR commented Feb 26, 2026

Goal

Reduce Vercel serverless function invocations on the quizzes list page by converting it from SSR to ISR. Quiz list is public data (same for everyone), user progress is private — split them.

Scope

  • New: /api/quiz/progress API route — returns user's quiz progress map (or {} for guests)
  • quizzes/page.tsx — removed force-dynamic, getCurrentUser(), getUserQuizzesProgress(); added revalidate = 300; fixed nested <main><section>
  • QuizzesSection.tsx — client-side progress fetch on mount; useState for active category; window.history.replaceState for URL sync; forceMount on TabsContent to prevent layout shift
  • GitHubStarButton.tsx — cache stars count in sessionStorage to skip refetch and counter animation on revisit

Expected impact

  • Quizzes page served from ISR cache instead of SSR per request
  • Note: full ISR effect blocked until force-dynamic is removed from [locale]/layout.tsx (tracked in _dev-notes/layout-force-dynamic-removal.md)

Out of scope

  • Removing force-dynamic from locale layout (separate task, SP: 3)
  • Tab switching layout shift (Radix TabsContent — mitigated with forceMount)

Summary by CodeRabbit

  • New Features

    • Quiz progress now loads asynchronously per visitor via an API, enabling live progress updates without blocking the page.
    • Categories remain mounted and switch instantly for smoother browsing.
  • Performance Improvements

    • Page revalidation configured for periodic caching to improve render times.
    • GitHub star count now uses session caching to avoid repeated fetches and animations.

…for quizzes page

- Move user progress fetch from SSR to client-side API (/api/quiz/progress)
- Remove force-dynamic and getCurrentUser() from quizzes page
- Add revalidate=300 for ISR caching
- Use window.history.replaceState for tab URL sync (avoid Next.js navigation)
- Add forceMount to TabsContent to prevent layout shift on tab switch
- Fix nested <main> — use <section> inside DynamicGridBackground
- Cache GitHub stars count in sessionStorage to avoid refetch + re-animation
Fixes React 19 react-hooks/refs ESLint error — useRef.current cannot
be read during render. Uses useState(getStoredStars) to capture the
sessionStorage value once on mount instead.
@vercel
Copy link
Contributor

vercel bot commented Feb 26, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
devlovers-net Ignored Ignored Preview Feb 26, 2026 1:44pm

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

Refactors quiz progress fetching from server-render to client-side via a new API route; quizzes page switches from dynamic to ISR (revalidate = 300); QuizzesSection now loads progress on mount; GitHub star count gains sessionStorage caching to avoid refetching.

Changes

Cohort / File(s) Summary
Quizzes page
frontend/app/[locale]/quizzes/page.tsx
Removed server-side user/session progress logic; page now exports revalidate = 300 and passes an empty userProgressMap to QuizzesSection.
Progress API route
frontend/app/api/quiz/progress/route.ts
Added new GET route that returns per-user quiz progress map (or {} if unauthenticated); exports runtime = 'nodejs' and uses no-store caching.
QuizzesSection component
frontend/components/quiz/QuizzesSection.tsx
Moved progress loading client-side: adds state (progressMap, progressLoaded, activeCategory), fetches /api/quiz/progress on mount, uses window.history.replaceState for category changes, and supplies loaded progress to QuizCard.
GitHub star UI
frontend/components/shared/GitHubStarButton.tsx
Introduced sessionStorage caching for star counts (read/write), skip network fetch when cached, adjust animation gating and minor icon transition tweak.

Sequence Diagram

sequenceDiagram
    participant Browser as Client (Browser)
    participant Page as Quizzes Page
    participant QS as QuizzesSection
    participant API as /api/quiz/progress
    participant Auth as Auth Service
    participant DB as Database

    Browser->>Page: Request /quizzes
    Page->>QS: Mount (userProgressMap = {})
    QS->>QS: init state (progressMap={}, progressLoaded=false, activeCategory)
    QS->>API: GET /api/quiz/progress
    API->>Auth: Check current user session
    alt authenticated
        Auth->>API: Return user
        API->>DB: Query user's quiz progress
        DB->>API: Return progress rows
        API->>API: Transform rows -> {quizId: {bestScore,totalQuestions,attemptsCount}}
        API-->>QS: 200 JSON (progress map) (cache: no-store)
    else unauthenticated
        Auth->>API: No user
        API-->>QS: 200 JSON {} (cache: no-store)
    end
    QS->>QS: set progressMap, progressLoaded=true
    QS-->>Browser: Render quizzes with userProgress (when available)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

performance

Suggested reviewers

  • ViktorSvertoka
  • AM1007

Poem

🐇 I hopped from server into client land,
Fetching progress with a nimble hand.
Stars remembered in session store,
Quizzes load faster than before —
A little rabbit clap and stand! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 accurately describes the main changes: ISR implementation for the quizzes page, client-side progress loading, and GitHub stars caching.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sl/feat/quiz

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
frontend/app/api/quiz/progress/route.ts (1)

17-26: Missing error handling for database query.

If getUserQuizzesProgress throws (e.g., database connection issue), the route returns a 500 error without any graceful handling. Consider wrapping in try-catch to return an appropriate error response or empty object with error logging.

🛡️ Proposed error handling
+  try {
     const rawProgress = await getUserQuizzesProgress(user.id);
     const progressMap: Record<string, { bestScore: number; totalQuestions: number; attemptsCount: number }> = {};

     for (const [quizId, progress] of rawProgress) {
       progressMap[quizId] = {
         bestScore: progress.bestScore,
         totalQuestions: progress.totalQuestions,
         attemptsCount: progress.attemptsCount,
       };
     }

     return NextResponse.json(progressMap, {
       headers: { 'Cache-Control': 'no-store' },
     });
+  } catch (error) {
+    console.error('Failed to fetch quiz progress:', error);
+    return NextResponse.json({}, {
+      status: 500,
+      headers: { 'Cache-Control': 'no-store' },
+    });
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/api/quiz/progress/route.ts` around lines 17 - 26, Wrap the call
to getUserQuizzesProgress and the following loop in a try-catch inside the route
handler (the block that builds progressMap) so database errors are handled
gracefully; on catch, log the error (using the existing logger or console) and
return an appropriate Response (e.g., JSON with an empty progressMap or an error
message and an HTTP error status) instead of letting the exception bubble up,
ensuring progressMap remains defined and the route returns a well-formed
response.
frontend/components/quiz/QuizzesSection.tsx (1)

3-3: Unused import: useRouter.

router is declared on line 43 but no longer used after switching to window.history.replaceState.

♻️ Remove unused import
-import { useParams, useRouter, useSearchParams } from 'next/navigation';
+import { useParams, useSearchParams } from 'next/navigation';

And remove line 43:

-  const router = useRouter();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/quiz/QuizzesSection.tsx` at line 3, Remove the unused
useRouter import and the unused router variable: delete useRouter from the
import list (the line importing useParams/useSearchParams) and remove the
declaration/assignment of router (the variable named router around line 43)
since you now use window.history.replaceState; update the import to only include
the hooks actually used (useParams, useSearchParams) to eliminate the unused
import warning in QuizzesSection.tsx.
frontend/components/shared/GitHubStarButton.tsx (2)

70-72: Silent catch block hides potential storage errors.

The empty catch {} block silently swallows errors. While sessionStorage failures are typically non-critical, consider logging in development for debugging purposes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/shared/GitHubStarButton.tsx` around lines 70 - 72, The
empty catch around sessionStorage.setItem(STORAGE_KEY, String(finalCount)) hides
storage errors; update the catch to log the error in non-production builds
(e.g., check process.env.NODE_ENV !== 'production' or a __DEV__ flag) and
include context (STORAGE_KEY and finalCount) using console.warn or similar, so
failures in GitHubStarButton.tsx are visible during development while still
swallowing in production.

16-22: Potential hydration mismatch from synchronous sessionStorage access during render.

Reading sessionStorage outside of useEffect can cause a mismatch between server-rendered HTML (where sessionStorage is undefined) and the initial client render. Although the typeof sessionStorage !== 'undefined' check prevents errors, the server renders with cached.current = null while the client may initialize with a cached value, causing displayCount and finalCount to differ.

Consider deferring the sessionStorage read to a useEffect or using a two-pass render pattern to avoid hydration warnings.

♻️ Suggested refactor to avoid hydration mismatch
-  const cached = useRef<number | null>(null);
-
-  if (cached.current === null && typeof sessionStorage !== 'undefined') {
-    const stored = sessionStorage.getItem(STORAGE_KEY);
-    if (stored) cached.current = parseInt(stored, 10);
-  }
-
-  const [displayCount, setDisplayCount] = useState(cached.current ?? 0);
-  const [finalCount, setFinalCount] = useState<number | null>(cached.current);
+  const cached = useRef<number | null>(null);
+  const [displayCount, setDisplayCount] = useState(0);
+  const [finalCount, setFinalCount] = useState<number | null>(null);
+
+  useEffect(() => {
+    if (typeof sessionStorage !== 'undefined') {
+      const stored = sessionStorage.getItem(STORAGE_KEY);
+      if (stored) {
+        const parsed = parseInt(stored, 10);
+        if (!Number.isNaN(parsed)) {
+          cached.current = parsed;
+          setDisplayCount(parsed);
+          setFinalCount(parsed);
+        }
+      }
+    }
+  }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/shared/GitHubStarButton.tsx` around lines 16 - 22, The
code reads sessionStorage during render causing possible hydration mismatches;
move the sessionStorage read and cached.current assignment into a useEffect and
initialize displayCount and finalCount to values stable between server and
client (e.g., 0 and null) so the server-rendered markup matches the initial
client render. Specifically, remove the synchronous sessionStorage access around
STORAGE_KEY from the render path, add a useEffect that reads sessionStorage,
parses the stored value, assigns cached.current, and calls setDisplayCount and
setFinalCount to update state; ensure references to cached.current,
displayCount, finalCount and STORAGE_KEY are updated only inside the effect to
avoid hydration warnings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/components/quiz/QuizzesSection.tsx`:
- Around line 57-67: The effect that fetches '/api/quiz/progress' currently only
calls setProgressLoaded(true) when the returned object has keys, so guests or
users with empty progress never leave the "loading" state; inside the useEffect
handling the fetch (the anonymous function that calls
fetch('/api/quiz/progress')), always call setProgressLoaded(true) after a
successful response (setProgressMap to the parsed data or {} even if empty) and
also ensure the .catch handler sets setProgressLoaded(true) (and optionally
leaves progressMap as {}), so setProgressLoaded is updated regardless of whether
data is empty or an error occurred; update references to setProgressMap and
setProgressLoaded in that block accordingly.

In `@frontend/components/shared/GitHubStarButton.tsx`:
- Line 18: The line using parseInt can produce NaN from corrupted
sessionStorage; change it to parse and validate before assigning: read stored,
do const parsed = parseInt(stored, 10) (or Number(stored)), check
Number.isNaN(parsed) and only set cached.current = parsed when it's a valid
number; if parsed is NaN, either leave cached.current unchanged (or reset to a
safe default) and optionally clear the bad sessionStorage entry—apply this to
the code around cached.current and stored in the GitHubStarButton component.

---

Nitpick comments:
In `@frontend/app/api/quiz/progress/route.ts`:
- Around line 17-26: Wrap the call to getUserQuizzesProgress and the following
loop in a try-catch inside the route handler (the block that builds progressMap)
so database errors are handled gracefully; on catch, log the error (using the
existing logger or console) and return an appropriate Response (e.g., JSON with
an empty progressMap or an error message and an HTTP error status) instead of
letting the exception bubble up, ensuring progressMap remains defined and the
route returns a well-formed response.

In `@frontend/components/quiz/QuizzesSection.tsx`:
- Line 3: Remove the unused useRouter import and the unused router variable:
delete useRouter from the import list (the line importing
useParams/useSearchParams) and remove the declaration/assignment of router (the
variable named router around line 43) since you now use
window.history.replaceState; update the import to only include the hooks
actually used (useParams, useSearchParams) to eliminate the unused import
warning in QuizzesSection.tsx.

In `@frontend/components/shared/GitHubStarButton.tsx`:
- Around line 70-72: The empty catch around sessionStorage.setItem(STORAGE_KEY,
String(finalCount)) hides storage errors; update the catch to log the error in
non-production builds (e.g., check process.env.NODE_ENV !== 'production' or a
__DEV__ flag) and include context (STORAGE_KEY and finalCount) using
console.warn or similar, so failures in GitHubStarButton.tsx are visible during
development while still swallowing in production.
- Around line 16-22: The code reads sessionStorage during render causing
possible hydration mismatches; move the sessionStorage read and cached.current
assignment into a useEffect and initialize displayCount and finalCount to values
stable between server and client (e.g., 0 and null) so the server-rendered
markup matches the initial client render. Specifically, remove the synchronous
sessionStorage access around STORAGE_KEY from the render path, add a useEffect
that reads sessionStorage, parses the stored value, assigns cached.current, and
calls setDisplayCount and setFinalCount to update state; ensure references to
cached.current, displayCount, finalCount and STORAGE_KEY are updated only inside
the effect to avoid hydration warnings.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e3526f and 98a0387.

📒 Files selected for processing (4)
  • frontend/app/[locale]/quizzes/page.tsx
  • frontend/app/api/quiz/progress/route.ts
  • frontend/components/quiz/QuizzesSection.tsx
  • frontend/components/shared/GitHubStarButton.tsx

@ViktorSvertoka ViktorSvertoka merged commit 3a59b4e into develop Feb 26, 2026
6 checks passed
@ViktorSvertoka ViktorSvertoka deleted the sl/feat/quiz branch February 26, 2026 13:49
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/components/shared/GitHubStarButton.tsx (1)

33-49: ⚠️ Potential issue | 🟠 Major

Validate parsed star counts before setFinalCount to prevent NaN propagation.

parseInt(normalized, 10) or parseFloat() can return NaN if the API returns an unexpected format (e.g., "abc"). A NaN value stored in finalCount breaks the animation: the increment becomes NaN, and the loop condition current >= finalCount (comparing NaN >= NaN) is always false, causing the interval to never clear. This creates a memory leak and broken UI.

Add validation before state update: coerce invalid or negative values to 0.

Suggested fix
         const response = await fetch('/api/stats');
         if (response.ok) {
           const data = await response.json();
           const starsStr =
             typeof data?.githubStars === 'string'
               ? data.githubStars
               : String(data?.githubStars ?? '0');
-          let starsNum = 0;
+          let starsNum: number;
           const normalized = starsStr.replace(/,/g, '').toLowerCase();
           if (normalized.includes('k+')) {
             starsNum = Math.floor(
               parseFloat(normalized.replace('k+', '')) * 1000
             );
           } else {
-            starsNum = parseInt(normalized, 10);
+            starsNum = Number.parseInt(normalized, 10);
           }
-          setFinalCount(starsNum);
+          setFinalCount(
+            Number.isFinite(starsNum) && starsNum >= 0 ? starsNum : 0
+          );
+        } else {
+          setFinalCount(0);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/shared/GitHubStarButton.tsx` around lines 33 - 49, The
parsed star count logic in the GitHubStarButton component can produce NaN or
negative values (from parseInt/parseFloat on unexpected API strings), so
validate starsNum before calling setFinalCount: after computing starsNum from
starsStr/normalized, check Number.isFinite(starsNum) && starsNum > 0 and if not,
set starsNum = 0 (or coerce to Math.max(0, Math.floor(starsNum)) for safety) and
then call setFinalCount with the sanitized value; update any dependent logic
that reads finalCount to assume a non-NaN non-negative integer.
🧹 Nitpick comments (2)
frontend/components/quiz/QuizzesSection.tsx (2)

3-3: Unused useRouter import and variable.

useRouter is imported and router is assigned but never used after the change to window.history.replaceState. Consider removing to reduce bundle size and avoid confusion.

🧹 Proposed fix
-import { useParams, useRouter, useSearchParams } from 'next/navigation';
+import { useParams, useSearchParams } from 'next/navigation';

And remove line 43:

-  const router = useRouter();

Also applies to: 43-43

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/quiz/QuizzesSection.tsx` at line 3, Remove the unused
Next router import and variable: delete useRouter from the import list in
QuizzesSection.tsx and remove the local router assignment (the line that calls
useRouter() and assigns to router), since the code now uses
window.history.replaceState and router is unused; update imports to only include
useParams and useSearchParams to avoid dead code and reduce bundle size.

51-67: Inconsistent indentation in state declarations and useEffect.

The code has irregular indentation that affects readability:

  • Lines 53-55: Extra leading spaces before const
  • Lines 57-67: The useEffect body is not properly indented relative to the component
🧹 Proposed formatting fix
   const [progressMap, setProgressMap] =
     useState<Record<string, UserProgress>>(userProgressMap);
-    const [progressLoaded, setProgressLoaded] = useState(
+  const [progressLoaded, setProgressLoaded] = useState(
     Object.keys(userProgressMap).length > 0
   );
-  
-   useEffect(() => {
-  fetch('/api/quiz/progress')
-    .then(res => res.ok ? res.json() : {})
-    .then(data => {
-      setProgressMap(data);
-      setProgressLoaded(true);
-    })
-    .catch(() => {
-      setProgressLoaded(true);
-    });
+
+  useEffect(() => {
+    fetch('/api/quiz/progress')
+      .then(res => (res.ok ? res.json() : {}))
+      .then(data => {
+        setProgressMap(data);
+        setProgressLoaded(true);
+      })
+      .catch(() => {
+        setProgressLoaded(true);
+      });
   }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/quiz/QuizzesSection.tsx` around lines 51 - 67, The state
declarations and the useEffect block have inconsistent leading spaces and
misaligned indentation; fix by aligning the const declarations for
progressMap/setProgressMap and progressLoaded/setProgressLoaded with the
surrounding JSX/component scope and reformat the entire useEffect callback
(fetch, then, catch, and setProgress* calls) so its opening line and inner lines
share consistent indentation (match the file's standard, e.g., two spaces) and
the closing bracket aligns with the component code; target symbols: progressMap,
setProgressMap, progressLoaded, setProgressLoaded, and the useEffect(...) block.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/components/shared/GitHubStarButton.tsx`:
- Around line 9-14: getStoredStars currently accesses sessionStorage.getItem
without error handling which can throw during render (it's used as
useState(getStoredStars)); wrap the read in a try/catch similar to the write
path so any thrown errors (private browsing, quota, CSP) are caught and the
function returns null, i.e., inside getStoredStars catch exceptions from
sessionStorage.getItem and return null; reference getStoredStars,
useState(getStoredStars) and sessionStorage.getItem to locate the change.

---

Outside diff comments:
In `@frontend/components/shared/GitHubStarButton.tsx`:
- Around line 33-49: The parsed star count logic in the GitHubStarButton
component can produce NaN or negative values (from parseInt/parseFloat on
unexpected API strings), so validate starsNum before calling setFinalCount:
after computing starsNum from starsStr/normalized, check
Number.isFinite(starsNum) && starsNum > 0 and if not, set starsNum = 0 (or
coerce to Math.max(0, Math.floor(starsNum)) for safety) and then call
setFinalCount with the sanitized value; update any dependent logic that reads
finalCount to assume a non-NaN non-negative integer.

---

Nitpick comments:
In `@frontend/components/quiz/QuizzesSection.tsx`:
- Line 3: Remove the unused Next router import and variable: delete useRouter
from the import list in QuizzesSection.tsx and remove the local router
assignment (the line that calls useRouter() and assigns to router), since the
code now uses window.history.replaceState and router is unused; update imports
to only include useParams and useSearchParams to avoid dead code and reduce
bundle size.
- Around line 51-67: The state declarations and the useEffect block have
inconsistent leading spaces and misaligned indentation; fix by aligning the
const declarations for progressMap/setProgressMap and
progressLoaded/setProgressLoaded with the surrounding JSX/component scope and
reformat the entire useEffect callback (fetch, then, catch, and setProgress*
calls) so its opening line and inner lines share consistent indentation (match
the file's standard, e.g., two spaces) and the closing bracket aligns with the
component code; target symbols: progressMap, setProgressMap, progressLoaded,
setProgressLoaded, and the useEffect(...) block.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 98a0387 and 90504bb.

📒 Files selected for processing (2)
  • frontend/components/quiz/QuizzesSection.tsx
  • frontend/components/shared/GitHubStarButton.tsx

Comment on lines +9 to +14
function getStoredStars(): number | null {
if (typeof sessionStorage === 'undefined') return null;
const stored = sessionStorage.getItem(STORAGE_KEY);
if (!stored) return null;
const parsed = parseInt(stored, 10);
return Number.isNaN(parsed) ? null : parsed;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n frontend/components/shared/GitHubStarButton.tsx | head -100

Repository: DevLoversTeam/devlovers.net

Length of output: 4129


🏁 Script executed:

# Check if there are other storage access patterns in the codebase
rg -n 'sessionStorage|localStorage' frontend/ --type tsx --type ts | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 98


🏁 Script executed:

# Check for any storage error handling patterns elsewhere
rg -n 'try\s*\{' frontend/components/shared/GitHubStarButton.tsx -A 3

Repository: DevLoversTeam/devlovers.net

Length of output: 349


Guard sessionStorage.getItem() with try/catch—read path lacks error handling that write path has.

getStoredStars() is used as a lazy initializer in useState(getStoredStars) (line 23), so it executes during render. The function currently checks only typeof sessionStorage === 'undefined', which prevents SSR but does not catch runtime errors from:

  • Private/incognito browsing (some browsers throw on storage access)
  • Storage quota exceeded
  • CSP restrictions or sandboxed iframes

The write operation at line 74 wraps sessionStorage.setItem() in try/catch, but the read at line 11 lacks the same protection. This asymmetry creates a render-blocking failure if sessionStorage.getItem() throws.

Add try/catch around the storage read to match the write path's error handling:

Suggested fix
 function getStoredStars(): number | null {
-  if (typeof sessionStorage === 'undefined') return null;
-  const stored = sessionStorage.getItem(STORAGE_KEY);
-  if (!stored) return null;
-  const parsed = parseInt(stored, 10);
-  return Number.isNaN(parsed) ? null : parsed;
+  if (typeof window === 'undefined') return null;
+  try {
+    const stored = window.sessionStorage.getItem(STORAGE_KEY);
+    if (!stored) return null;
+    const parsed = Number.parseInt(stored, 10);
+    return Number.isNaN(parsed) ? null : parsed;
+  } catch {
+    return null;
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/shared/GitHubStarButton.tsx` around lines 9 - 14,
getStoredStars currently accesses sessionStorage.getItem without error handling
which can throw during render (it's used as useState(getStoredStars)); wrap the
read in a try/catch similar to the write path so any thrown errors (private
browsing, quota, CSP) are caught and the function returns null, i.e., inside
getStoredStars catch exceptions from sessionStorage.getItem and return null;
reference getStoredStars, useState(getStoredStars) and sessionStorage.getItem to
locate the change.

ViktorSvertoka added a commit that referenced this pull request Feb 26, 2026
* (SP: 3) [Backend] add Nova Poshta shipping foundation + checkout persistence + async label workflow (#364)

* (SP: 2) [Frontend] Reduce Vercel variable costs via caching and analytics cleanup (#367)

* perf(vercel): cut runtime costs via notification, blog cache, and analytics changes

* perf(blog): remove server searchParams usage to preserve ISR

* fix(build): align Netlify Node version and remove SpeedInsights import

* chore(release): bump version to 1.0.4

* (SP: 2) [Frontend] Remove [locale] layout force-dynamic and move auth to client-side (#370)

* refactor(frontend): remove locale layout dynamic auth and move header auth client-side

* fix(frontend): prevent stale auth responses in useAuth and remove redundant dashboard dynamic layout

* (SP: 2) [Frontend] Reduce auth overhead and sync auth state across tabs (#372)

* refactor(frontend): remove locale layout dynamic auth and move header auth client-side

* fix(frontend): prevent stale auth responses in useAuth and remove redundant dashboard dynamic layout

* feat(frontend): sync auth state across tabs via BroadcastChannel

* (SP: 2) [Frontend] Quizzes page ISR + client-side progress + GitHub stars cache (#371)

* perf(quiz-flow): move quiz progress to client-side fetch, enable ISR for quizzes page

- Move user progress fetch from SSR to client-side API (/api/quiz/progress)
- Remove force-dynamic and getCurrentUser() from quizzes page
- Add revalidate=300 for ISR caching
- Use window.history.replaceState for tab URL sync (avoid Next.js navigation)
- Add forceMount to TabsContent to prevent layout shift on tab switch
- Fix nested <main> — use <section> inside DynamicGridBackground
- Cache GitHub stars count in sessionStorage to avoid refetch + re-animation

* perf: replace useRef with useState lazy initializer in GitHubStarButton

Fixes React 19 react-hooks/refs ESLint error — useRef.current cannot
be read during render. Uses useState(getStoredStars) to capture the
sessionStorage value once on mount instead.

* fix: stop star icon trembling on hover in GitHubStarButton

* (SP: 1) [Frontend] Fix quiz timer flash and card layout shift on quizzes page (#373)

* perf(quiz-flow): move quiz progress to client-side fetch, enable ISR for quizzes page

- Move user progress fetch from SSR to client-side API (/api/quiz/progress)
- Remove force-dynamic and getCurrentUser() from quizzes page
- Add revalidate=300 for ISR caching
- Use window.history.replaceState for tab URL sync (avoid Next.js navigation)
- Add forceMount to TabsContent to prevent layout shift on tab switch
- Fix nested <main> — use <section> inside DynamicGridBackground
- Cache GitHub stars count in sessionStorage to avoid refetch + re-animation

* perf: replace useRef with useState lazy initializer in GitHubStarButton

Fixes React 19 react-hooks/refs ESLint error — useRef.current cannot
be read during render. Uses useState(getStoredStars) to capture the
sessionStorage value once on mount instead.

* fix: stop star icon trembling on hover in GitHubStarButton

* fix: eliminate quiz timer flash on language switch

Remove Suspense boundary (loading.tsx) that unmounted QuizContainer
during locale navigation. Synchronous session restore via useReducer
lazy initializer and correct timer initialization via useState lazy
initializer prevent any visible state reset on language switch

* fix: replace quiz card layout shift with skeleton grid during progress load

* chore(release): v1.0.5

---------

Co-authored-by: Liudmyla Sovetovs <milkaegik@gmail.com>
Co-authored-by: Lesia Soloviova <106915140+LesiaUKR@users.noreply.github.com>
ViktorSvertoka added a commit that referenced this pull request Feb 27, 2026
* (SP: 3) [Backend] add Nova Poshta shipping foundation + checkout persistence + async label workflow (#364)

* (SP: 2) [Frontend] Reduce Vercel variable costs via caching and analytics cleanup (#367)

* perf(vercel): cut runtime costs via notification, blog cache, and analytics changes

* perf(blog): remove server searchParams usage to preserve ISR

* fix(build): align Netlify Node version and remove SpeedInsights import

* chore(release): bump version to 1.0.4

* (SP: 2) [Frontend] Remove [locale] layout force-dynamic and move auth to client-side (#370)

* refactor(frontend): remove locale layout dynamic auth and move header auth client-side

* fix(frontend): prevent stale auth responses in useAuth and remove redundant dashboard dynamic layout

* (SP: 2) [Frontend] Reduce auth overhead and sync auth state across tabs (#372)

* refactor(frontend): remove locale layout dynamic auth and move header auth client-side

* fix(frontend): prevent stale auth responses in useAuth and remove redundant dashboard dynamic layout

* feat(frontend): sync auth state across tabs via BroadcastChannel

* (SP: 2) [Frontend] Quizzes page ISR + client-side progress + GitHub stars cache (#371)

* perf(quiz-flow): move quiz progress to client-side fetch, enable ISR for quizzes page

- Move user progress fetch from SSR to client-side API (/api/quiz/progress)
- Remove force-dynamic and getCurrentUser() from quizzes page
- Add revalidate=300 for ISR caching
- Use window.history.replaceState for tab URL sync (avoid Next.js navigation)
- Add forceMount to TabsContent to prevent layout shift on tab switch
- Fix nested <main> — use <section> inside DynamicGridBackground
- Cache GitHub stars count in sessionStorage to avoid refetch + re-animation

* perf: replace useRef with useState lazy initializer in GitHubStarButton

Fixes React 19 react-hooks/refs ESLint error — useRef.current cannot
be read during render. Uses useState(getStoredStars) to capture the
sessionStorage value once on mount instead.

* fix: stop star icon trembling on hover in GitHubStarButton

* (SP: 1) [Frontend] Fix quiz timer flash and card layout shift on quizzes page (#373)

* perf(quiz-flow): move quiz progress to client-side fetch, enable ISR for quizzes page

- Move user progress fetch from SSR to client-side API (/api/quiz/progress)
- Remove force-dynamic and getCurrentUser() from quizzes page
- Add revalidate=300 for ISR caching
- Use window.history.replaceState for tab URL sync (avoid Next.js navigation)
- Add forceMount to TabsContent to prevent layout shift on tab switch
- Fix nested <main> — use <section> inside DynamicGridBackground
- Cache GitHub stars count in sessionStorage to avoid refetch + re-animation

* perf: replace useRef with useState lazy initializer in GitHubStarButton

Fixes React 19 react-hooks/refs ESLint error — useRef.current cannot
be read during render. Uses useState(getStoredStars) to capture the
sessionStorage value once on mount instead.

* fix: stop star icon trembling on hover in GitHubStarButton

* fix: eliminate quiz timer flash on language switch

Remove Suspense boundary (loading.tsx) that unmounted QuizContainer
during locale navigation. Synchronous session restore via useReducer
lazy initializer and correct timer initialization via useState lazy
initializer prevent any visible state reset on language switch

* fix: replace quiz card layout shift with skeleton grid during progress load

* chore(release): v1.0.5

* (SP: 3)[Shop][DB] Reduce Neon compute: throttle janitor + relax checkout polling + add sweep indexes (#375)

* (SP: 3) [Backend] add internal janitor (jobs 1-4), claim/lease + runbook (G0-G6)

* (SP: 3) [Backend] add provider selector, fix payments gating, i18n checkout errors

* Add shop category images to public

* (SP: 3) [Shop][Monobank] I1 structured logging: codes + logging safety checks

* (SP: 3) [Shop][Monobank] Fail-closed non-browser origin posture for webhook + janitor (ORIGIN_BLOCKED)

* (SP: 3) [Shop][Monobank] [Shop][Monobank] J gate: add orders status ownership test and pass all pre-prod invariants

* (SP: 3) [Shop][Monobank]  review fixes (tests, logging, success UI)

* (SP: 1) [Shop][Monobank] Tighten webhook log-code typing; harden DB tests; minor security/log/UI cleanups

* (SP: 1) [Shop][Monobank] harden Monobank webhook (origin/PII-safe logs) and remove duplicate sha256 hashing

* (SP: 1) [Cart] adding route for user orders to cart page

* (SP: 1) [Cart] fix after review cart mpage and adding index for orders

* (SP: 1) [Cart] Fix cart orders summary auth rendering and return totalCount for orders badge

* (SP: 1) [Cart] remove console.warn from CartPageClient to satisfy monobank logging safety invariant, namespace localStorage cart by user and reset on auth change

* (SP: 1) [Cart] rehydrate per cartOwnerId (remove didHydrate coupling)

* (SP: 2)[Backend] shop/shipping schema migrations foundation

* (SP: 2)[Backend] shop/shipping public routes + np cache + sync

* (SP: 2)[Backend] shop/shipping: shipping persistence + currency policy

* (SP: 2)[Backend] shop/shipping: webhook apply + psp fields + enqueue shipping

* (SP: 2)[Backend] shop/shipping: shipments worker + internal run + np mock

* (SP: 2)[Backend] shop/shipping: admin+ui shipping actions

* (SP: 2)[Backend] shop/shipping: retention + log sanitizer + metrics

* (SP: 1)[Backend] stabilize Monobank janitor (job1/job3) and fix failing apply-outcomes tests

* (SP: 1) [db]: add shop shipping core migration

* (SP: 1) [FIX] resolve merge artifacts in order details page

* (SP: 1) [FIX] apply post-review fixes for shipping and admin flows

* (SP: 1) [FIX] align cart shipping imports (localeToCountry + availability reason code)

* (SP: 1) [FIX] hard-block checkout when shipping disabled + i18n reason mapping

* (SP: 1) [FIX] harden webhook enqueue + shipping worker + NP catalog + cart fail-closed

* (SP: 1) [FIX] Initialize shippingMethodsLoading to true to avoid premature checkout.

* (SP: 1) [FIX] migration 17

* (SP: 1) [DB] migrarion to testind DB and adjusting tests

* (SP: 1)[DB] slow down restock janitor + enforce prod interval floor

* (SP: 1) [DB] add order status lite view (opt-in) + instrumentation

* (SP: 1) [DB] replace checkout success router.refresh polling with backoff API polling

* (SP: 1) [DB] throttle sessions activity heartbeat + use count(*) (PK invariant)

* (SP: 1)[DB] enforce production min intervals for internal shipping jobs

* (SP: 1) [DB] add minimal partial indexes for orders sweeps + rollout notes

* (SP: 1) [DB] refactor sweep claim step to FOR UPDATE SKIP LOCKED batching

* (SP: 1)[DB]: slow janitor schedule to every 30 minutes

* (SP: 1)[DB] increase polling delays for MonobankRedirectStatus

* (SP: 1)[FIX] harden webhooks + fix SSR hydration + janitor/np gates + sweeps refactor

* (SP: 1)[FIX] harden shipping enqueue gating + apply NP interval floor

---------

Co-authored-by: Liudmyla Sovetovs <milkaegik@gmail.com>
Co-authored-by: Lesia Soloviova <106915140+LesiaUKR@users.noreply.github.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