Skip to content

Conversation

@al-imam
Copy link
Collaborator

@al-imam al-imam commented Nov 10, 2025

Bookmark icons sync issue

Fixes: QF-3097

This PR fixes an issue where deleting a bookmark or removing an ayah from a collection did not correctly update the bookmark icon and collection checkbox on the ayah page. The ayah remained visually "bookmarked" even after deletion, causing user confusion and red error messages when attempting to re-toggle the bookmark state.

The issue was caused by stale SWR cache entries not being invalidated for deleted bookmarks and their associated collection records.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

Test plan

  1. Creating a bookmark for an ayah.
  2. Going to my Profile → Collections page.
  3. Deleting that ayah from the collection.
  4. Returning to the same ayah page.
  5. Verified that the bookmark icon and collection checkbox were no longer active, confirming that the state is correctly synced.

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • My changes generate no new warnings
  • Any dependent changes have been merged and published in downstream modules
  • I have commented on my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Summary by CodeRabbit

  • Bug Fixes
    • Improved cache invalidation for bookmark operations to ensure bookmark state is consistently synchronized across the application.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 10, 2025

Walkthrough

This PR adds SWR cache invalidation logic for bookmark operations. When bookmarks are deleted, the components now compute the mushafId, retrieve affected bookmark data, and explicitly delete corresponding cache entries from both the single bookmark and bookmark collections URLs.

Changes

Cohort / File(s) Summary
Bookmark Cache Invalidation
src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx, src/components/Verses/BookmarkedVersesList.tsx
Added SWR cache management imports and invalidation logic. Modified bookmark deletion handlers to compute mushafId, retrieve bookmark URLs, and delete affected cache entries. Updated onUpdated callback to accept bookmarkId parameter for targeted cache invalidation.

Sequence Diagram

sequenceDiagram
    participant User
    participant Component
    participant API
    participant Cache

    rect rgb(200, 220, 255)
    Note over User,Cache: Before: Bookmark Deletion (Without Cache Invalidation)
    User->>Component: Delete Bookmark
    Component->>API: Send deletion request
    API-->>Component: Success
    Cache->>Cache: ❌ Stale cache remains
    end

    rect rgb(220, 240, 220)
    Note over User,Cache: After: Bookmark Deletion (With Cache Invalidation)
    User->>Component: Delete Bookmark
    Component->>Component: Compute mushafId from styles
    Component->>API: Send deletion request
    API-->>Component: Success
    Component->>Cache: cache.delete(bookmarkUrl)
    Component->>Cache: cache.delete(collectionsUrl)
    Cache->>Cache: ✓ Stale cache cleared
    end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

  • Focus areas: Cache invalidation logic implementation and mushafId computation patterns in both files to ensure consistency and correctness of cache key construction

Poem

🐰 Hops through the cache with glee,
Deleting stale bookmarks with SWR spree,
No ghosts of old data remain to roam,
Fresh data blooms in memory's home! 📚✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately identifies the bug being fixed (bookmark icons sync issue) and includes the JIRA ticket reference, clearly summarizing the main change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The pull request description follows the template structure with all required sections filled out completely and relevant to the changes made.
✨ 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 QF-3097-bookmark-icons-sync

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: 4

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7887b1 and 2fa93e4.

📒 Files selected for processing (2)
  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx (6 hunks)
  • src/components/Verses/BookmarkedVersesList.tsx (5 hunks)
🧰 Additional context used
📓 Path-based instructions (13)
**/*.*

📄 CodeRabbit inference engine (.cursor/rules/bug-handling-with-todo-comments.mdc)

If you encounter a bug in existing code, or the instructions lead to suboptimal or buggy code, add comments starting with "TODO:" outlining the problems.

**/*.*: Utilize Early Returns: Use early returns to avoid nested conditions and improve readability.
Conditional Classes: Prefer conditional classes over ternary operators for class attributes.

**/*.*: Use comments sparingly, and when you do, make them meaningful.
Don't comment on obvious things. Excessive or unclear comments can clutter the codebase and become outdated.
Use comments to convey the 'why' behind specific actions or explain unusual behavior and potential pitfalls.
Provide meaningful information about the function's behavior and explain unusual behavior and potential pitfalls.

**/*.*: Write short functions that only do one thing.
Follow the single responsibility principle (SRP), which means that a function should have one purpose and perform it effectively.
If a function becomes too long or complex, consider breaking it into smaller, more manageable functions.

Order functions with those that are composing other functions appearing earlier in the file. For example, if you have a menu with multiple buttons, define the menu function above the buttons.

**/*.*: Always add helpful comments to the code explaining what you are doing.
Never delete old comments, unless they are no longer relevant because the code has been rewritten or deleted.

**/*.*: Choose names for variables, functions, and classes that reflect their purpose and behavior.
A name should tell you why it exists, what it does, and how it is used. If a name requires a comment, then the name does not reveal its intent.
Use specific names that provide a clearer understanding of what the variables represent and how they are used.

Files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
  • src/components/Verses/BookmarkedVersesList.tsx
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/next-js-conventions.mdc)

**/*.{js,jsx,ts,tsx}: Rely on Next.js Pages Router for state changes.
Minimize 'use client' usage: Prefer server components and Next.js SSR features.
Minimize 'use client' usage: Use 'use client' only for Web API access in small components.
Minimize 'use client' usage: Avoid using 'use client' for data fetching or state management.

**/*.{js,jsx,ts,tsx}: Optimize Web Vitals (LCP, CLS, FID)
Use dynamic loading for non-critical components using @src/components/dls/Spinner/Spinner.tsx

Files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
  • src/components/Verses/BookmarkedVersesList.tsx
src/components/**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/react-functional-components.mdc)

src/components/**/*.tsx: Always use React functional components with hooks.
Use React.FC for functional components with props.

Files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
  • src/components/Verses/BookmarkedVersesList.tsx
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (.cursor/rules/react.mdc)

**/*.{tsx,jsx}: Use functional components over class components
Keep components small and focused
Extract reusable logic into custom hooks
Use composition over inheritance
Split large components into smaller, focused ones
Follow the Rules of Hooks
Use custom hooks for reusable logic
Use appropriate dependency arrays in useEffect
Implement cleanup in useEffect when needed
Avoid nested hooks
Use useState for local component state
Avoid prop drilling through proper state management
Implement proper memoization (useMemo, useCallback)
Use React.memo for expensive components
Avoid unnecessary re-renders
Implement proper lazy loading
Use proper key props in lists
Profile and optimize render performance
Show appropriate loading and error states
Handle async errors properly
Show user-friendly error messages
Implement proper fallback UI
Log errors appropriately
Handle edge cases gracefully
Use semantic HTML elements
Implement proper ARIA attributes
Ensure keyboard navigation
Handle focus management
Provide proper alt text for images
Use proper imports/exports
Document complex component logic

Files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
  • src/components/Verses/BookmarkedVersesList.tsx
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/react.mdc)

Implement proper prop types with TypeScript

**/*.tsx: Prefix interfaces for React props with 'Props' (e.g., ButtonProps)
Implement proper error boundaries

React: use functional components only; extract reusable logic into custom hooks; apply proper memoization (React.memo/useMemo/useCallback)

Files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
  • src/components/Verses/BookmarkedVersesList.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/react.mdc)

Use xstate for complex state logic

**/*.{ts,tsx}: TypeScript: prefer interfaces over type aliases for object shapes; avoid any; add explicit return types for public functions
Error handling: define and use custom error types; use async/await correctly; show user-friendly error messages
Use Redux Toolkit for state management and XState for complex state machines
Implement i18n with next-translate in components and utilities

Files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
  • src/components/Verses/BookmarkedVersesList.tsx
**/*.{ts,tsx,d.ts}

📄 CodeRabbit inference engine (.cursor/rules/typescript.mdc)

**/*.{ts,tsx,d.ts}: Prefer interfaces over types for object definitions
Use type for unions, intersections, and mapped types
Avoid using any, prefer unknown for unknown types
Leverage TypeScript's built-in utility types
Use generics for reusable type patterns
Use PascalCase for type names and interfaces
Use camelCase for variables and functions
Use UPPER_CASE for constants
Use descriptive names with auxiliary verbs (e.g., isLoading, hasError)
Use explicit return types for public functions
Use arrow functions for callbacks and methods
Implement proper error handling with custom error types
Use function overloads for complex type scenarios
Prefer async/await over Promises
Use readonly for immutable properties
Leverage discriminated unions for type safety
Use type guards for runtime type checking
Implement proper null checking
Avoid type assertions unless necessary
Create custom error types for domain-specific errors
Use Result types for operations that can fail
Use try-catch blocks with typed catch clauses
Handle Promise rejections properly

Files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
  • src/components/Verses/BookmarkedVersesList.tsx
{src,types}/**/*.ts?(x)

📄 CodeRabbit inference engine (CLAUDE.md)

Use TypeScript throughout the codebase with strict configuration

Files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
  • src/components/Verses/BookmarkedVersesList.tsx
src/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer React functional components for UI implementation

Files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
  • src/components/Verses/BookmarkedVersesList.tsx
{src,types}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use path aliases @/ for src and @/dls/* for design system imports

Files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
  • src/components/Verses/BookmarkedVersesList.tsx
src/components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Organize React components by feature and place DLS components under src/components/dls

Files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
  • src/components/Verses/BookmarkedVersesList.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Order imports: React first, then external, then internal; alphabetize within groups with blank lines between groups
Naming: camelCase for variables/functions; PascalCase for types, interfaces, and React components; UPPER_CASE for constants
Functions should be <= 30 lines, prefer early returns, single responsibility, and descriptive names

Files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
  • src/components/Verses/BookmarkedVersesList.tsx
**/*.{ts,tsx,js,jsx,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Use Prettier formatting: single quotes, 2-space indentation, 100-character line width, and trailing commas

Files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
  • src/components/Verses/BookmarkedVersesList.tsx
🧠 Learnings (13)
📚 Learning: 2025-10-28T09:59:48.894Z
Learnt from: afifvdin
Repo: quran/quran.com-frontend-next PR: 2557
File: src/components/QuranReader/ReflectionView/ReflectionBodyContainer/ReflectionBody/index.tsx:88-0
Timestamp: 2025-10-28T09:59:48.894Z
Learning: In src/components/QuranReader/ReflectionView/ReflectionBodyContainer/ReflectionBody/index.tsx, the empty `<div className={styles.separatorContainer} />` rendered when `isModal` is true is required for maintaining consistent spacing in modal view and should not be removed.

Applied to files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
📚 Learning: 2025-09-19T13:57:41.697Z
Learnt from: mohsinayoob
Repo: quran/quran.com-frontend-next PR: 2495
File: src/utils/auth/constants.ts:37-50
Timestamp: 2025-09-19T13:57:41.697Z
Learning: In the quran.com-frontend-next codebase, prefer TypeScript's readonly modifier over Object.freeze() for array immutability when compile-time protection is sufficient.

Applied to files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
📚 Learning: 2025-10-22T20:33:55.176Z
Learnt from: yousefdergham
Repo: quran/quran.com-frontend-next PR: 2547
File: src/components/Course/CourseDetails/Tabs/Syllabus/index.tsx:44-49
Timestamp: 2025-10-22T20:33:55.176Z
Learning: Preference: In quran.com-frontend-next, keep concise “why” comments for analytics/UX decision points (e.g., guest vs enrolled click handlers) but avoid verbose JSDoc that repeats params/types. Convert multi-line JSDoc to brief inline rationale comments, especially in src/components/Course/CourseDetails/Tabs/Syllabus/index.tsx and similar analytics handlers.

Applied to files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
📚 Learning: 2025-10-22T20:42:09.214Z
Learnt from: yousefdergham
Repo: quran/quran.com-frontend-next PR: 2547
File: src/pages/learning-plans/[slug]/lessons/[lessonSlugOrId]/index.tsx:35-59
Timestamp: 2025-10-22T20:42:09.214Z
Learning: Preference for quran.com-frontend-next: keep single-use, page-specific components inline. Example: NotEnrolledMessage in src/pages/learning-plans/[slug]/lessons/[lessonSlugOrId]/index.tsx should remain inline; extract only if reused elsewhere.

Applied to files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
📚 Learning: 2025-08-01T08:50:03.171Z
Learnt from: CR
Repo: quran/quran.com-frontend-next PR: 0
File: .cursor/rules/next-js-conventions.mdc:0-0
Timestamp: 2025-08-01T08:50:03.171Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Rely on Next.js Pages Router for state changes.

Applied to files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
📚 Learning: 2025-08-01T08:49:45.154Z
Learnt from: CR
Repo: quran/quran.com-frontend-next PR: 0
File: .cursor/rules/next-js-app-router-rule.mdc:0-0
Timestamp: 2025-08-01T08:49:45.154Z
Learning: Applies to app/**/*.tsx : Follow Next.js documentation for best practices in data fetching, rendering, and routing

Applied to files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
  • src/components/Verses/BookmarkedVersesList.tsx
📚 Learning: 2025-10-07T08:38:28.343Z
Learnt from: CR
Repo: quran/quran.com-frontend-next PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T08:38:28.343Z
Learning: Applies to pages/**/*.{ts,tsx} : Use Next.js Pages Router patterns (do not use App Router)

Applied to files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
📚 Learning: 2025-10-07T08:38:28.343Z
Learnt from: CR
Repo: quran/quran.com-frontend-next PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T08:38:28.343Z
Learning: Applies to **/*.{ts,tsx} : Implement i18n with next-translate in components and utilities

Applied to files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
📚 Learning: 2025-10-18T11:22:53.139Z
Learnt from: jonybekov
Repo: quran/quran.com-frontend-next PR: 2537
File: src/redux/slices/session.ts:36-42
Timestamp: 2025-10-18T11:22:53.139Z
Learning: In Redux selectors, avoid calling functions that read from external state (cookies, localStorage, etc.) like `isLoggedIn()` from utils/auth/login. Instead, use reactive hooks like `useIsLoggedIn` directly in components and keep selectors focused only on Redux state to ensure proper memoization.

Applied to files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
  • src/components/Verses/BookmarkedVersesList.tsx
📚 Learning: 2025-10-25T12:14:44.062Z
Learnt from: yousefdergham
Repo: quran/quran.com-frontend-next PR: 2547
File: src/pages/learning-plans/[slug]/lessons/[lessonSlugOrId]/index.tsx:58-64
Timestamp: 2025-10-25T12:14:44.062Z
Learning: In quran.com-frontend-next, router.query values (like slug) are not considered reactive dependencies by the project's react-hooks/exhaustive-deps rule and don't need to be included in useEffect dependency arrays. Toast functions from useToast() are also stable and don't need to be in deps.

Applied to files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
  • src/components/Verses/BookmarkedVersesList.tsx
📚 Learning: 2025-10-20T15:04:10.434Z
Learnt from: afifvdin
Repo: quran/quran.com-frontend-next PR: 2540
File: src/components/Verse/VerseAndTranslation.tsx:8-11
Timestamp: 2025-10-20T15:04:10.434Z
Learning: In the quran.com-frontend-next codebase, the Error component imported from '@/components/Error' should not be renamed to avoid shadowing the global Error object, as this is the established convention consistently used across the codebase.

Applied to files:

  • src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
📚 Learning: 2025-10-19T11:34:07.609Z
Learnt from: afifvdin
Repo: quran/quran.com-frontend-next PR: 2540
File: src/components/Verse/PlainVerseText/index.tsx:4-4
Timestamp: 2025-10-19T11:34:07.609Z
Learning: In src/components/Verse/PlainVerseText/index.tsx, the translation for verse titles should be wired directly to the component using next-translate rather than passed as props from parent components, as this ensures the wording from locales is centralized and only needs to be changed in one place.

Applied to files:

  • src/components/Verses/BookmarkedVersesList.tsx
📚 Learning: 2025-10-07T08:38:28.343Z
Learnt from: CR
Repo: quran/quran.com-frontend-next PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-07T08:38:28.343Z
Learning: Applies to **/*.tsx : React: use functional components only; extract reusable logic into custom hooks; apply proper memoization (React.memo/useMemo/useCallback)

Applied to files:

  • src/components/Verses/BookmarkedVersesList.tsx
🧬 Code graph analysis (2)
src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx (3)
src/redux/slices/QuranReader/styles.ts (1)
  • selectQuranReaderStyles (141-141)
src/utils/api.ts (1)
  • getMushafId (65-77)
src/utils/auth/apiPaths.ts (2)
  • makeBookmarkUrl (200-206)
  • makeBookmarkCollectionsUrl (175-181)
src/components/Verses/BookmarkedVersesList.tsx (6)
src/components/dls/Toast/Toast.tsx (1)
  • useToast (86-96)
src/redux/slices/QuranReader/bookmarks.ts (1)
  • selectBookmarks (63-63)
src/utils/api.ts (1)
  • getMushafId (65-77)
src/utils/auth/login.ts (1)
  • isLoggedIn (17-17)
src/utils/auth/apiPaths.ts (3)
  • makeBookmarksUrl (62-63)
  • makeBookmarkUrl (200-206)
  • makeBookmarkCollectionsUrl (175-181)
src/utils/auth/api.ts (1)
  • deleteBookmarkById (440-442)
🔇 Additional comments (7)
src/components/Verses/BookmarkedVersesList.tsx (4)

10-10: LGTM: Cache invalidation imports added correctly.

The new imports support the cache invalidation fix and are properly organized.

Also applies to: 20-20, 23-27


40-40: LGTM: SWR cache access configured correctly.


48-53: LGTM: Centralized URL construction improves consistency.

Using makeBookmarksUrl helper ensures consistent API URL construction across the codebase.


80-124: LGTM: Cache invalidation logic correctly implemented.

The bookmark deletion flow is well-structured:

  1. Early return prevents errors when bookmark not found
  2. mutate() revalidates the bookmarks list
  3. Cache entries for both bookmark URL and bookmark collections URL are invalidated
  4. Error handling with user-friendly toast message

The cache invalidation ensures the UI reflects the deletion immediately when users navigate to the ayah page.

src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx (3)

6-7: LGTM: Cache invalidation imports added correctly.

The new imports support the cache invalidation fix and maintain consistency with the pattern used in BookmarkedVersesList.tsx.

Also applies to: 19-22


61-64: LGTM: Redux and SWR configuration set up correctly.

Using shallowEqual with the selector prevents unnecessary re-renders, following React best practices.


114-136: LGTM: Bookmark deletion handlers correctly pass bookmarkId.

Both deletion paths (direct bookmark deletion and collection-bookmark removal) now pass bookmarkId to onUpdated, enabling targeted cache invalidation.

Comment on lines +1 to +2
/* eslint-disable max-lines */

Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider refactoring to address max-lines violation.

The max-lines disable suggests this component exceeds recommended size limits. Per coding guidelines, components should follow the single responsibility principle and be split when they become too complex.

🤖 Prompt for AI Agents
In
src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
lines 1-2, the file currently disables the max-lines rule indicating the
component is too large; refactor it by extracting distinct responsibilities into
smaller modules: move complex hooks and data-fetching logic into a custom hook
file (e.g., useCollectionDetail.ts), pull out large render blocks into
presentational subcomponents (e.g., CollectionHeader, CollectionMetadata,
CollectionItems) placed in the same folder, relocate any helper functions or
constants to a utils file, and keep the main container focused on orchestration
and state composition while updating imports and tests accordingly.

Comment on lines +80 to 95
const onUpdated = (bookmarkId: string) => {
mutate();

const mushafId = getMushafId(quranReaderStyles.quranFont, quranReaderStyles.mushafLines).mushaf;
const bookmark = bookmarks.find((b) => b.id === bookmarkId);

if (bookmark) {
cache.delete(
makeBookmarkUrl(mushafId, bookmark.key, BookmarkType.Ayah, bookmark.verseNumber),
);

cache.delete(
makeBookmarkCollectionsUrl(mushafId, bookmark.key, BookmarkType.Ayah, bookmark.verseNumber),
);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

LGTM: Cache invalidation correctly implemented.

The cache invalidation logic properly:

  1. Revalidates data with mutate()
  2. Computes mushafId from current styles
  3. Finds the deleted bookmark and invalidates both cache entries

The conditional cache deletion is acceptable since the bookmark may not be in the currently loaded pages (due to pagination).

Optional: Consider memoizing mushafId at component level for consistency.

For consistency with the broader caching strategy and to avoid recomputing mushafId on every deletion:

+const mushafId = useMemo(
+  () => getMushafId(quranReaderStyles.quranFont, quranReaderStyles.mushafLines).mushaf,
+  [quranReaderStyles.quranFont, quranReaderStyles.mushafLines]
+);
+
 const onUpdated = (bookmarkId: string) => {
   mutate();
-  const mushafId = getMushafId(quranReaderStyles.quranFont, quranReaderStyles.mushafLines).mushaf;
   const bookmark = bookmarks.find((b) => b.id === bookmarkId);
📝 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 onUpdated = (bookmarkId: string) => {
mutate();
const mushafId = getMushafId(quranReaderStyles.quranFont, quranReaderStyles.mushafLines).mushaf;
const bookmark = bookmarks.find((b) => b.id === bookmarkId);
if (bookmark) {
cache.delete(
makeBookmarkUrl(mushafId, bookmark.key, BookmarkType.Ayah, bookmark.verseNumber),
);
cache.delete(
makeBookmarkCollectionsUrl(mushafId, bookmark.key, BookmarkType.Ayah, bookmark.verseNumber),
);
}
};
const mushafId = useMemo(
() => getMushafId(quranReaderStyles.quranFont, quranReaderStyles.mushafLines).mushaf,
[quranReaderStyles.quranFont, quranReaderStyles.mushafLines]
);
const onUpdated = (bookmarkId: string) => {
mutate();
const bookmark = bookmarks.find((b) => b.id === bookmarkId);
if (bookmark) {
cache.delete(
makeBookmarkUrl(mushafId, bookmark.key, BookmarkType.Ayah, bookmark.verseNumber),
);
cache.delete(
makeBookmarkCollectionsUrl(mushafId, bookmark.key, BookmarkType.Ayah, bookmark.verseNumber),
);
}
};
🤖 Prompt for AI Agents
In
src/components/Collection/CollectionDetailContainer/CollectionDetailContainer.tsx
around lines 80 to 95, memoize the computed mushafId instead of recomputing it
inside onUpdated: compute mushafId once at the component level (e.g., using
useMemo or a ref) based on quranReaderStyles.quranFont and
quranReaderStyles.mushafLines and reference that memoized value within
onUpdated; ensure the memo updates when the relevant quranReaderStyles change so
cache deletion uses a stable, consistent mushafId.

Comment on lines +1 to +5
/* eslint-disable max-lines, react-func/max-lines-per-function */
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/anchor-is-valid */

Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider refactoring to remove eslint-disable directives.

Multiple eslint-disable directives indicate code quality concerns:

  • max-lines suggests the component exceeds recommended size limits
  • Accessibility disables (jsx-a11y/*) may impact keyboard navigation and screen reader support

Per coding guidelines, components should be small, focused, and accessible. Consider splitting this component and addressing the accessibility issues.

🤖 Prompt for AI Agents
In src/components/Verses/BookmarkedVersesList.tsx lines 1-5, the file currently
disables multiple ESLint rules (max-lines and jsx-a11y rules); refactor to
remove these disables by splitting the large component into smaller focused
subcomponents (e.g., header, list item, controls) so each file stays under
max-lines, restore the jsx-a11y rules and replace non-accessible elements with
semantic equivalents (use <button> or <a href> instead of plain divs/anchors
without href), add proper keyboard handlers (onKeyDown) and tabIndex where
needed, and include relevant ARIA attributes/roles so keyboard and screen-reader
interaction is preserved; run ESLint and adjust minor behavior into the
extracted components until all disabled rules can be removed.

Comment on lines +46 to +47
const mushafId = getMushafId(quranReaderStyles.quranFont, quranReaderStyles.mushafLines).mushaf;

Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider memoizing mushafId for performance.

The mushafId is recomputed on every render. While the performance impact is minimal, wrapping it in useMemo with quranReaderStyles as a dependency would follow best practices.

+const mushafId = useMemo(
+  () => getMushafId(quranReaderStyles.quranFont, quranReaderStyles.mushafLines).mushaf,
+  [quranReaderStyles.quranFont, quranReaderStyles.mushafLines]
+);
-const mushafId = getMushafId(quranReaderStyles.quranFont, quranReaderStyles.mushafLines).mushaf;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/Verses/BookmarkedVersesList.tsx around lines 46 to 47,
mushafId is recomputed on every render; wrap the call to getMushafId(...) in a
useMemo, using quranReaderStyles (or specific properties used) as the dependency
array so mushafId is only recalculated when styles change, and ensure you import
useMemo from React if not already.

@AhmedCodeGuy
Copy link
Collaborator

AhmedCodeGuy commented Nov 10, 2025

PR Review

Hey! Thanks for working on this bug fix. I've reviewed the changes and found some issues that need addressing before we can merge. The good news is they're straightforward to fix! 👍


🚨 Critical Issue: cache.delete() Won't Work

The main problem: Using cache.delete() doesn't trigger revalidation, so the UI won't update in real-time. This means the original bug won't actually be fixed.

Files affected:

  • CollectionDetailContainer.tsx:88-94
  • BookmarkedVersesList.tsx:88-104

Why this matters:
If a user deletes a bookmark from the Collections page while viewing that ayah in another tab, the bookmark icon won't disappear until they refresh. The bug persists.

The fix:

// ❌ Current - doesn't notify components
cache.delete(makeBookmarkUrl(...));

// ✅ Use this instead
globalMutate(makeBookmarkUrl(...));

mutate() triggers revalidation and updates all subscribed components. You already have useSWRConfig() imported, so just use mutate instead of cache.

Reference: SaveToCollectionAction.tsx:103-110 already does this correctly - follow that pattern.


🔧 Required Fixes

1. Extract duplicate cache invalidation logic

You're repeating the same cache invalidation code in both files. Let's DRY it up:

// Create: src/utils/cache/bookmarkCache.ts
export const invalidateBookmarkCache = (
  mutate: (key: string) => void,
  mushafId: number,
  bookmark: { key: number; verseNumber: number }
) => {
  mutate(makeBookmarkUrl(mushafId, bookmark.key, BookmarkType.Ayah, bookmark.verseNumber));
  mutate(makeBookmarkCollectionsUrl(mushafId, bookmark.key, BookmarkType.Ayah, bookmark.verseNumber));
};

// Usage in components:
invalidateBookmarkCache(globalMutate, mushafId, bookmark);

2. Memoize mushafId in CollectionDetailContainer

Currently calculating it on every delete. If the user changes mushaf type mid-session, you'll invalidate the wrong cache key.

// Move to component level (line ~60)
const mushafId = useMemo(
  () => getMushafId(quranReaderStyles.quranFont, quranReaderStyles.mushafLines).mushaf,
  [quranReaderStyles.quranFont, quranReaderStyles.mushafLines]
);

// Then just use mushafId in onUpdated

3. Add error feedback for missing bookmark

Good guard clause, but it fails silently. Users should know something went wrong:

if (!selectedBookmark) {
  console.warn('Bookmark not found:', verseKey);
  toast(t('common:error.bookmark-not-found'), { status: ToastStatus.Error });
  return;
}

4. Remove ESLint disables

Instead of adding /* eslint-disable max-lines */, extract a helper function:

const findBookmarkByVerseKey = (verseKey: string, bookmarks: Bookmark[]) => {
  const [chapter, verseNumber] = getVerseAndChapterNumbersFromKey(verseKey);
  return bookmarks.find(
    b => b.key === Number(chapter) && b.verseNumber === Number(verseNumber)
  );
};

This keeps your functions under the line limit and makes the code more readable.


5. Add unit tests

Please add tests for the cache invalidation:

describe('BookmarkedVersesList', () => {
  it('should invalidate cache when deleting bookmark', async () => {
    const mockMutate = jest.fn();
    // Test that mutate is called with correct keys
  });

  it('should show error when bookmark not found', () => {
    // Test the guard clause
  });
});

⚠️ Edge Cases to Consider

Multi-tab sync

Test this: Open ayah in Tab A, delete bookmark from Collections in Tab B. Tab A should update automatically. (Currently fails with cache.delete())

Collection vs bookmark deletion

When removing an ayah from one collection but it's still in another, make sure the bookmark icon stays visible. The current cache invalidation might be too aggressive.

Race conditions

If someone rapidly deletes multiple bookmarks, the mutate() calls might interfere with each other. Consider testing this scenario.


🎯 Summary

What needs to change:

  1. Replace cache.delete() with mutate() (critical!)
  2. Extract cache invalidation to utility function
  3. Memoize mushafId
  4. Add error toast for missing bookmarks
  5. Refactor to remove ESLint disables
  6. Add unit tests

Why it matters:
The current implementation won't fix the bug because cache.delete() doesn't update mounted components. Switching to mutate() will make everything work as expected.

Next steps:

  1. Make the fixes above
  2. Run the testing checklist
  3. Push changes for re-review

Let me know if you have questions or need clarification on any of these points! Happy to help. 🙌

@AhmedCodeGuy
Copy link
Collaborator

AhmedCodeGuy commented Nov 10, 2025

Testing Checklist

This is the manual testing checklist to make there are no bugs or regressions, those will be covered with end to end testing using playwright in the future In shaa Allah, but for the time being we need to test them manually.

✅ Quick Smoke Tests

Basic Bookmark Deletion

  • Go to /1/1 and bookmark the ayah
  • Navigate to Profile → Bookmarks
  • Delete the bookmark
  • Go back to /1/1
  • VERIFY: Bookmark icon is unmarked

Collection Deletion

  • Go to /1/2 and add to a collection
  • Navigate to Profile → Collections → [Collection Name]
  • Delete the ayah from collection
  • Go back to /1/2
  • VERIFY: Collection checkbox is unchecked

🔍 Critical Tests (Must Pass)

Test 1: Multi-Tab Sync (CRITICAL!)

  • Open Tab A: Navigate to /1/1 and bookmark
  • Open Tab B: Navigate to Profile → Bookmarks
  • In Tab B, delete the bookmark
  • Switch to Tab A (DO NOT REFRESH)
  • VERIFY: Bookmark icon automatically updates to unmarked

Current Status: ❌ Fails with cache.delete(), should pass with mutate()


Test 2: Multi-Collection Scenario

  • Navigate to /1/1
  • Create Collection A and add ayah
  • Create Collection B and add same ayah
  • Go to Collections → Collection A
  • Remove ayah from Collection A only
  • Navigate back to /1/1
  • VERIFY: Bookmark icon STILL marked (in Collection B)
  • VERIFY: Collection A checkbox unchecked
  • VERIFY: Collection B checkbox still checked

Test 3: Full Delete vs Remove from Collection

Path A - Full Delete:

  • Add ayah to ONE collection only
  • On Collections detail page, check "Delete bookmark"
  • Delete the ayah
  • VERIFY: Bookmark removed from collection
  • VERIFY: Bookmark deleted from all bookmarks
  • VERIFY: Icon unmarked on ayah page

Path B - Remove from Collection:

  • Add ayah to TWO collections
  • On Collections detail page, uncheck "Delete bookmark"
  • Remove the ayah
  • VERIFY: Bookmark removed from THIS collection only
  • VERIFY: Bookmark still exists in other collection
  • VERIFY: Icon still marked on ayah page

⚠️ Edge Case Tests

Network Failure Handling

  • Navigate to Profile → Bookmarks
  • Open DevTools → Network → Set to "Offline"
  • Try to delete a bookmark
  • VERIFY: Error toast appears
  • VERIFY: Bookmark remains in list
  • VERIFY: No console errors

Rapid Deletion

  • Create 5 bookmarks
  • Navigate to Profile → Bookmarks
  • Rapidly click delete on all 5 (as fast as possible)
  • VERIFY: All bookmarks deleted
  • VERIFY: No duplicates remain
  • VERIFY: No error messages

Translation View vs Reading View

  • In Reading View, bookmark /1/1
  • Switch to Translation View
  • Verify bookmark icon shows
  • Delete bookmark from Translation View
  • Switch back to Reading View
  • VERIFY: Icon updates in both views
  • VERIFY: State stays consistent

🌐 Browser Compatibility

Test on at least 2 browsers:

  • Chrome (latest)
  • Safari (latest)
  • Firefox (optional)
  • Mobile (optional)

📊 Regression Checks

Verify these still work after changes:

  • Adding bookmarks
  • Creating collections
  • Adding to collections
  • Viewing bookmarks list
  • Bookmark icons display correctly

✨ Test Results

  • Quick Tests: ☐ Pass ☐ Fail
  • Critical Tests: ☐ Pass ☐ Fail
  • Edge Cases: ☐ Pass ☐ Fail
  • Regression: ☐ Pass ☐ Fail

@al-imam
Copy link
Collaborator Author

al-imam commented Nov 23, 2025

Brother "Ahmed Hussein" handling this ticket

@al-imam al-imam closed this Nov 23, 2025
@al-imam al-imam deleted the QF-3097-bookmark-icons-sync branch November 23, 2025 09:50
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.

3 participants