Skip to content

Refactor/courses cleanup#71

Merged
AustinKelsay merged 17 commits intomainfrom
refactor/courses-cleanup
May 12, 2025
Merged

Refactor/courses cleanup#71
AustinKelsay merged 17 commits intomainfrom
refactor/courses-cleanup

Conversation

@AustinKelsay
Copy link
Copy Markdown
Owner

@AustinKelsay AustinKelsay commented May 11, 2025

Summary by CodeRabbit

  • New Features

    • Introduced new course-related components: CourseContent (lesson display with transitions), CourseOverview (course details and completion status), and CourseQA (Q&A/comments section).
    • Added a custom MarkdownEditor with enhanced dark mode support.
    • Implemented new React hooks for course data, lessons, navigation, payment, decryption, and tab state management.
  • Improvements

    • Replaced forced page reloads with smooth client-side navigation in course sidebar.
    • Refactored course page to use modular hooks and components, improving maintainability and user experience.
    • Updated all forms to use the new MarkdownEditor for a consistent editing experience.
    • Adjusted import paths for components to match new directory structure.
  • Chores

    • Added the "buffer" package as a dependency.
    • Updated layout styling for the BottomBar component for improved spacing.

@vercel
Copy link
Copy Markdown

vercel bot commented May 11, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
plebdevs ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 12, 2025 5:10pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented May 11, 2025

Walkthrough

This update introduces and refactors several course-related React hooks and components to modularize course data fetching, lesson management, decryption, navigation, and UI rendering. It replaces inline logic in the course page with composable hooks and components, updates import paths, and standardizes the markdown editor across multiple forms with a new dark-mode-compatible editor component.

Changes

File(s) Change Summary
src/components/content/courses/details/CourseDetails.js
src/components/navbar/Navbar.js
src/pages/course/[slug]/draft/index.js
Updated import paths for components to reflect new directory structure or use absolute aliases; no logic changes.
src/components/content/courses/layout/CourseSidebar.js Replaced full page reload navigation with client-side function call for smoother lesson selection.
src/components/content/courses/tabs/CourseContent.js
src/components/content/courses/tabs/CourseOverview.js
src/components/content/courses/tabs/CourseQA.js
Introduced new React components for displaying course content, overview, and Q&A/comments, each with clearly defined props and conditional rendering logic.
src/components/forms/combined/CombinedResourceForm.js
src/components/forms/combined/EditDraftCombinedResourceForm.js
src/components/forms/combined/EditPublishedCombinedResourceForm.js
src/components/forms/course/embedded/EmbeddedDocumentForm.js
src/components/forms/document/DocumentForm.js
src/components/forms/document/EditDraftDocumentForm.js
src/components/forms/document/EditPublishedDocumentForm.js
src/components/forms/video/EditPublishedVideoForm.js
Replaced dynamic import of external markdown editor with a new local MarkdownEditor component and updated related imports and JSX; standardized dark mode support.
src/components/markdown/MarkdownEditor.js Added new MarkdownEditor React component with dark mode styling, dynamic import of editor, and customizable props.
src/hooks/courses/index.js New index module to re-export all course-related hooks for centralized importing.
src/hooks/courses/useCourseData.js
src/hooks/courses/useCourseNavigation.js
src/hooks/courses/useCoursePayment.js
src/hooks/courses/useCourseTabs.js
src/hooks/courses/useLessons.js
src/hooks/courses/useCourseTabsState.js
Introduced new custom hooks for fetching course data, managing navigation, handling payments, tab state, and lessons, each with clear state management and utility functions.
src/hooks/encryption/useCourseDecryption.js Added new hook to manage lesson decryption with retry and timeout logic, tracking decryption status per lesson.
src/pages/course/[slug]/index.js Refactored to remove inline logic for data, lessons, decryption, and navigation; now uses new hooks and components for modularized state and UI management.
src/pages/_app.js Wrapped BottomBar in a div to add responsive top margin; no logic changes.
package.json Added buffer dependency.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CoursePage
    participant useCourseData
    participant useLessons
    participant useCourseDecryption
    participant useCourseNavigation
    participant CourseOverview
    participant CourseContent
    participant CourseQA

    User->>CoursePage: Navigates to course page
    CoursePage->>useCourseData: Fetch course data and lesson IDs
    useCourseData-->>CoursePage: Returns course, lessonIds, paidCourse, loading
    CoursePage->>useLessons: Fetch lessons for lessonIds
    useLessons-->>CoursePage: Returns lessons, uniqueLessons
    CoursePage->>useCourseDecryption: Decrypt active lesson if needed
    useCourseDecryption-->>CoursePage: Returns decryption status
    CoursePage->>useCourseNavigation: Manage tab/lesson/sidebar state
    useCourseNavigation-->>CoursePage: Returns navigation state and handlers
    CoursePage->>CourseOverview: Render overview tab (if active)
    CoursePage->>CourseContent: Render content tab (if active)
    CoursePage->>CourseQA: Render QA/comments tab (if active)
    User->>CoursePage: Interacts with tabs/lessons
    CoursePage->>useCourseNavigation: Update navigation state
Loading

Possibly related PRs

  • AustinKelsay/plebdevs#70: Refactors the decryption hook to decrypt only the active lesson with retry and timeout logic, directly supporting the new hook usage in this PR.
  • AustinKelsay/plebdevs#11: Splits and introduces the combined resource form components, which are now referenced with updated import paths in this PR.
  • AustinKelsay/plebdevs#64: Refactors CourseDetails.js to use DesktopCourseDetails and MobileCourseDetails, directly related to the import path updates in this PR.

Poem

In the garden of code, a new course blooms bright,
With hooks for each lesson and tabs for delight.
Markdown now dances in dark mode attire,
Navigation is smooth—no more full page retire.
Rabbits rejoice as the modules align,
Hopping through lessons, the learning’s divine!
🐇✨

Tip

⚡️ Faster reviews with caching
  • CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.

Enjoy the performance boost—your workflow just got faster.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 403948f and c995048.

📒 Files selected for processing (1)
  • src/pages/_app.js (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/pages/_app.js (1)
src/components/BottomBar.js (1)
  • BottomBar (5-40)
🔇 Additional comments (1)
src/pages/_app.js (1)

34-36: Improved responsive layout for BottomBar.

This change properly adds a margin above the BottomBar component on mobile views while removing it at the custom min-bottom-bar breakpoint. This works well with the existing responsive behavior in the BottomBar component (which has min-bottom-bar:hidden class) to create a better user experience across different device sizes.

✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@AustinKelsay AustinKelsay marked this pull request as ready for review May 12, 2025 13:20
@AustinKelsay
Copy link
Copy Markdown
Owner Author

@CodeRabbit full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented May 12, 2025

✅ Actions performed

Full review triggered.

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

🧹 Nitpick comments (16)
src/config/appConfig.js (1)

14-15: Provide context for the new author public key
The raw hex string is valid (64‐char lowercase), but it’s unclear whose key this is and why it’s being added. Please add a short comment above the entry to explain its owner/purpose and verify that any authentication or test suites have been updated to include this key.

src/components/content/courses/details/CourseDetails.js (1)

22-23: Inconsistent import path styles.

While DesktopCourseDetails now uses an absolute path with the '@/' alias, MobileCourseDetails still uses a relative import. For consistency and maintainability, consider using the same import style for both components.

 // Import the desktop and mobile components
 import DesktopCourseDetails from '@/components/content/courses/details/DesktopCourseDetails';
-import MobileCourseDetails from './MobileCourseDetails';
+import MobileCourseDetails from '@/components/content/courses/details/MobileCourseDetails';
src/components/forms/course/embedded/EmbeddedDocumentForm.js (1)

2-17: Import organization includes unused dependencies

The import structure has been updated, but some imported modules don't appear to be used in the shown code. Specifically, axios, InputTextarea, Calendar, and useRouter are imported but not utilized in this component based on the provided code.

Consider removing or commenting unused imports to avoid bloat:

-import axios from 'axios';
-import { InputTextarea } from 'primereact/inputtextarea';
-import { Calendar } from 'primereact/calendar';
-import { useRouter } from 'next/router';
src/components/content/courses/tabs/CourseQA.js (1)

1-32: Well-structured comments component with hardcoded relay URLs

The CourseQA component is well-organized and provides a clear UI for the comments section. However, the relay URLs are hardcoded in the component, which could make future maintenance challenging.

Consider extracting the relay URLs to a configuration file or environment variable:

-            relays="wss://nos.lol/, wss://relay.damus.io/, wss://relay.snort.social/, wss://relay.nostr.band/, wss://relay.primal.net/, wss://nostrue.com/, wss://purplerelay.com/, wss://relay.devs.tools/"
+            relays={NOSTR_RELAY_URLS}

Then define NOSTR_RELAY_URLS in a constants file that can be imported.

src/hooks/courses/useLessons.js (1)

1-61: Well-implemented lessons hook with performance optimization opportunities

The useLessons hook effectively fetches and manages lesson data. It's well-documented and follows React patterns correctly.

Consider these performance improvements:

  1. Check if NDK is already connected before connecting:
        try {
-          await ndk.connect();
+          if (!ndk.connected) {
+            await ndk.connect();
+          }
  1. Fetch authors in parallel for better performance:
-          // Process events (no need to check for duplicates here)
-          for (const event of events) {
-            const author = await fetchAuthor(event.pubkey);
-            const parsedLesson = { ...parseEvent(event), author };
-            newLessons.push(parsedLesson);
-          }
+          // Process events in parallel
+          const lessonsPromises = Array.from(events).map(async (event) => {
+            const author = await fetchAuthor(event.pubkey);
+            return { ...parseEvent(event), author };
+          });
+          
+          const newLessons = await Promise.all(lessonsPromises);
  1. Consider adding error state handling rather than just logging errors:
        } catch (error) {
          console.error('Error fetching events:', error);
+          // Consider adding error state here
+          // setError(error);
        }
src/hooks/courses/useCoursePayment.js (1)

37-38: Simplify conditional expression with optional chaining

The condition can be simplified for better readability.

-    if (response && response?.preimage) {
+    if (response?.preimage) {
🧰 Tools
🪛 Biome (1.9.4)

[error] 37-37: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/hooks/courses/useCourseNavigation.js (1)

109-127: Update useEffect dependency array for keyboard navigation

The useEffect for keyboard navigation depends on tabMap but doesn't include the helper functions that use it.

-  }, [activeTab, tabMap]);
+  }, [activeTab, tabMap, toggleTab, getActiveTabIndex]);
src/hooks/courses/useCourseData.js (2)

39-54: Consider optimizing NDK connection management

The ndk.connect() is called on every fetch, which could be inefficient if the hook re-runs frequently.

Consider moving the NDK connection management to a higher level (app initialization or a dedicated hook) and ensuring it's only connected once:

 const fetchCourse = async courseId => {
   try {
-     await ndk.connect();
     const event = await ndk.fetchEvent({ '#d': [courseId] });
     if (!event) return null;

51-52: Enhance error handling with user feedback

Errors are logged to console but not shown to users, which limits visibility of issues.

     } catch (error) {
       console.error('Error fetching course:', error);
+      showToast('error', 'Error', 'Failed to load course data. Please try again.');
       return null;
     }
src/hooks/encryption/useCourseDecryption.js (1)

105-113: Avoid stale closures when updating lessons

setLessons(updatedLessons) relies on the lessons array captured when decryptCurrentLesson was created.
If another component mutates lessons before decryption finishes, those changes are lost (last‐write-wins bug).

-      const updatedLessons = lessons.map(lesson =>
+      const updatedLessons = (prevLessons) => prevLessons.map(lesson =>
         lesson.id === currentLesson.id
           ? { ...lesson, content: decryptedContent }
           : lesson
       );
-      setLessons(updatedLessons);
+      setLessons(updatedLessons);

Using the functional form merges the update with any concurrent state changes.

src/components/content/courses/tabs/CourseContent.js (1)

32-48: Nested transition timer is never cleared

Inside the effect that handles lesson switching, a second setTimeout (50 ms) is started inside the first (300 ms) but is not stored or cleared on re-render/unmount. While the leak is tiny, it can accumulate during quick navigation and, more importantly, may still call setIsTransitioning(false) after the component unmounts.

Quick fix:

-      const timer = setTimeout(() => {
+      const outerTimer = setTimeout(() => {
...
-        setTimeout(() => {
+        const innerTimer = setTimeout(() => {
           setIsTransitioning(false);
         }, 50);
+        // cleanup inner timer, too
+        return () => clearTimeout(innerTimer);
       }, 300);
       
-      return () => clearTimeout(timer);
+      return () => clearTimeout(outerTimer);

(Or refactor to a single CSS-only transition to remove JS timers entirely.)

src/hooks/courses/useCourseTabs.js (2)

81-104: Label/key mismatch may confuse analytics & QA

tabMap uses the key "qa" while getTabItems() returns the label "Comments".
Ensure naming consistency (e.g. use "qa" everywhere or rename to "comments").

-    items.push({
-      label: 'Comments',
+    items.push({
+      label: 'QA',

(or vice-versa)


15-16: Possible initial flicker on SSR / very narrow viewports

windowWidth is undefined on the first render, yielding
undefined <= 968 // false, so isMobileView starts as false.
This can cause a one-frame flash of the desktop layout before the hook recalculates.
Consider initialising to true when windowWidth is undefined:

-  const isMobileView = windowWidth <= 968;
+  const isMobileView = windowWidth === undefined ? true : windowWidth <= 968;
src/pages/course/[slug]/index.js (3)

70-86: Robustness for localStorage parsing & SSR edge-cases

Good job deferring the read to useEffect (client-only). Two small hardening tips:

  1. Guard for cases where window.localStorage is unavailable (private-mode Safari can throw).
  2. Prefix JSON.parse inside its own try/catch (you already do) and fall back to an empty array to keep state invariant.

No action strictly required, but worth considering.


88-106: Simplify duplicate-prevention logic in setCompleted

Current code does a manual includes check and then spreads the array. A Set keeps intent crystal-clear and guarantees uniqueness:

-      // Avoid duplicates
-      if (prev.includes(lessonId)) {
-        return prev;
-      }
-
-      const newCompletedLessons = [...prev, lessonId];
+      const newCompletedLessons = Array.from(
+        new Set([...prev, lessonId])
+      );

Functionality stays identical, complexity drops.


185-190: Memoise isAuthorized for readability

isAuthorized recomputes on every render even when none of its constituents change. Wrapping it in useMemo avoids needless work and self-documents the dependency list:

-  const isAuthorized = 
-    session?.user?.role?.subscribed || 
-    session?.user?.pubkey === course?.pubkey || 
-    !paidCourse || 
-    session?.user?.purchased?.some(purchase => purchase.courseId === course?.d);
+  const isAuthorized = React.useMemo(() => (
+    session?.user?.role?.subscribed ||
+    session?.user?.pubkey === course?.pubkey ||
+    !paidCourse ||
+    session?.user?.purchased?.some(p => p.courseId === course?.d)
+  ), [session, paidCourse, course]);

A micro-optimisation, but it clarifies intent and avoids accidental dependency omissions in the future.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c73d6eb and 9bd0c08.

📒 Files selected for processing (25)
  • src/components/content/courses/details/CourseDetails.js (1 hunks)
  • src/components/content/courses/layout/CourseSidebar.js (1 hunks)
  • src/components/content/courses/tabs/CourseContent.js (1 hunks)
  • src/components/content/courses/tabs/CourseOverview.js (1 hunks)
  • src/components/content/courses/tabs/CourseQA.js (1 hunks)
  • src/components/forms/combined/CombinedResourceForm.js (2 hunks)
  • src/components/forms/combined/EditDraftCombinedResourceForm.js (2 hunks)
  • src/components/forms/combined/EditPublishedCombinedResourceForm.js (3 hunks)
  • src/components/forms/course/embedded/EmbeddedDocumentForm.js (2 hunks)
  • src/components/forms/document/DocumentForm.js (2 hunks)
  • src/components/forms/document/EditDraftDocumentForm.js (2 hunks)
  • src/components/forms/document/EditPublishedDocumentForm.js (2 hunks)
  • src/components/forms/video/EditPublishedVideoForm.js (2 hunks)
  • src/components/markdown/MarkdownEditor.js (1 hunks)
  • src/components/navbar/Navbar.js (1 hunks)
  • src/config/appConfig.js (1 hunks)
  • src/hooks/courses/index.js (1 hunks)
  • src/hooks/courses/useCourseData.js (1 hunks)
  • src/hooks/courses/useCourseNavigation.js (1 hunks)
  • src/hooks/courses/useCoursePayment.js (1 hunks)
  • src/hooks/courses/useCourseTabs.js (1 hunks)
  • src/hooks/courses/useLessons.js (1 hunks)
  • src/hooks/encryption/useCourseDecryption.js (1 hunks)
  • src/pages/course/[slug]/draft/index.js (1 hunks)
  • src/pages/course/[slug]/index.js (8 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (7)
src/components/forms/combined/EditPublishedCombinedResourceForm.js (1)
src/components/markdown/MarkdownEditor.js (1)
  • MarkdownEditor (50-126)
src/components/forms/course/embedded/EmbeddedDocumentForm.js (1)
src/components/markdown/MarkdownEditor.js (1)
  • MarkdownEditor (50-126)
src/components/forms/document/DocumentForm.js (1)
src/components/markdown/MarkdownEditor.js (1)
  • MarkdownEditor (50-126)
src/components/forms/combined/EditDraftCombinedResourceForm.js (3)
src/components/markdown/MarkdownEditor.js (1)
  • MarkdownEditor (50-126)
src/components/forms/combined/CombinedResourceForm.js (2)
  • content (24-24)
  • handleContentChange (40-42)
src/components/forms/combined/EditPublishedCombinedResourceForm.js (2)
  • content (32-32)
  • handleContentChange (68-70)
src/components/forms/combined/CombinedResourceForm.js (1)
src/components/markdown/MarkdownEditor.js (1)
  • MarkdownEditor (50-126)
src/hooks/courses/useLessons.js (5)
src/pages/course/[slug]/index.js (2)
  • useLessons (125-130)
  • fetchAuthor (108-116)
src/components/content/courses/details/CourseDetails.js (2)
  • fetchAuthor (88-99)
  • author (36-36)
src/components/content/courses/details/DraftCourseDetails.js (2)
  • fetchAuthor (35-46)
  • author (23-23)
src/pages/course/[slug]/draft/index.js (2)
  • fetchAuthor (31-43)
  • lessons (15-15)
src/hooks/courses/useCourseData.js (1)
  • lessonIds (15-15)
src/hooks/courses/useCourseTabs.js (1)
src/hooks/useWindowWidth.js (1)
  • useWindowWidth (3-24)
🪛 Biome (1.9.4)
src/hooks/courses/useCoursePayment.js

[error] 37-37: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (26)
src/components/navbar/Navbar.js (1)

11-11: Import path updated to follow new directory structure.

The path for CourseHeader has been correctly updated to match the new organization of course components into a 'layout' subdirectory, consistent with the refactoring effort.

src/pages/course/[slug]/draft/index.js (1)

5-6: Import paths correctly updated to absolute paths.

The import paths for course components now use absolute paths with the '@/' alias, which is better for maintainability in deeply nested directories. This change aligns with the course components reorganization.

src/components/content/courses/layout/CourseSidebar.js (1)

40-42: Improved navigation with client-side transitions.

Great improvement! Replacing the direct page reload with the onLessonSelect callback enables smoother transitions between lessons without full page refreshes. This enhances user experience and aligns with React's component-based architecture.

src/components/forms/combined/EditDraftCombinedResourceForm.js (2)

8-14: Import reorganization with custom MarkdownEditor.

The imports have been reorganized for better logical grouping, and the custom MarkdownEditor component has been imported to replace the direct usage of the third-party markdown editor.


243-243: Enhanced markdown editing with consistent styling.

The custom MarkdownEditor component replaces the previous implementation, providing consistent dark mode styling across the application while maintaining the same functionality.

src/components/forms/document/EditDraftDocumentForm.js (2)

11-14: Added required CSS imports and custom MarkdownEditor.

The changes add necessary PrimeReact and PrimeIcons CSS imports that were previously missing, and replace the direct markdown editor import with the custom MarkdownEditor component.


146-146: Standardized markdown editor implementation.

The custom MarkdownEditor component replaces the previous implementation, ensuring consistent styling and behavior across all forms in the application.

src/hooks/courses/index.js (1)

1-13: Well-structured barrel file for course-related hooks.

This new file centralizes all course-related hooks in a single export, following the best practice of using barrel exports for related functionality. This makes imports cleaner in components that use multiple hooks.

The organization of related hooks into a single entry point improves code maintainability and supports the broader refactoring effort for course functionality seen in this PR.

src/components/forms/combined/CombinedResourceForm.js (2)

8-14: Import reorganization with custom MarkdownEditor.

The changes reorganize imports for better logical grouping and introduce the custom MarkdownEditor component, aligning with the pattern seen in other form components.


200-200: Standardized markdown editor implementation.

The implementation now uses the custom MarkdownEditor component, ensuring consistent styling and behavior across the application while maintaining the same functionality with the original props.

src/components/forms/document/DocumentForm.js (2)

12-14: Import organization improved

The import structure has been improved by consolidating related imports. The order change for primeicons and addition of the MarkdownEditor component follows a logical grouping pattern.


148-152: Enhanced markdown editing with custom component

The replacement of the direct MDEditor usage with the custom MarkdownEditor component improves consistency across the application and centralizes styling logic. The custom component already includes the dark mode styling that was previously applied through a wrapping div.

src/components/forms/document/EditPublishedDocumentForm.js (3)

3-7: Better organization of PrimeReact UI imports

The import statements are now better organized with PrimeReact UI components grouped together, improving code readability and maintainability.


10-18: Improved import structure with necessary styles

The imports now properly include the required CSS styles for PrimeReact components, and the custom MarkdownEditor component replaces the previous dynamic import. The hook import path has also been standardized.


201-201: Simplified markdown editor implementation

Replaced the complex implementation using direct MDEditor with a wrapper div for dark mode with the cleaner MarkdownEditor component that encapsulates these styling concerns.

src/components/forms/course/embedded/EmbeddedDocumentForm.js (1)

185-185: Enhanced markdown editing with custom component

The replacement of the direct MDEditor usage with the custom MarkdownEditor component improves consistency across the application and centralizes styling logic. The component properly receives the same props as before.

src/components/forms/video/EditPublishedVideoForm.js (3)

3-7: Better organization of PrimeReact UI imports

The import statements are now better organized with PrimeReact UI components grouped together, improving code readability and maintainability.


16-19: Improved import structure with necessary styles

The imports now properly include the required CSS styles for PrimeReact components and the custom MarkdownEditor component. This ensures consistent styling across the application.


192-192: Simplified video embed input with consistent editor component

The video embed input now uses the same MarkdownEditor component used throughout the application, ensuring a consistent editing experience across different content types. This is a good example of component reuse.

src/components/forms/combined/EditPublishedCombinedResourceForm.js (2)

3-7: Import organization improved

The imports have been nicely reorganized to group related PrimeReact components together and include necessary CSS files. This makes the dependencies clearer and more maintainable.

Also applies to: 10-10, 16-19


222-222: Standardized markdown editor implementation

Replaced the direct markdown editor implementation with the new MarkdownEditor component that provides consistent styling and behavior across the application. This improves maintainability and ensures a consistent user experience.

Also applies to: 239-239

src/components/content/courses/tabs/CourseOverview.js (1)

1-58: Well-structured course overview component

The CourseOverview component is well-organized with clean conditional rendering based on device view and good separation of concerns. The component properly handles both mobile and desktop views with appropriate styling adjustments.

src/hooks/courses/useCoursePayment.js (1)

1-67: LGTM! Well-structured hook implementation

This hook effectively manages course payment processing and authorization with proper memoization and separation of concerns. The implementation includes good error handling, proper use of React hooks, and comprehensive authorization checks.

🧰 Tools
🪛 Biome (1.9.4)

[error] 37-37: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/components/markdown/MarkdownEditor.js (1)

1-128: LGTM! Excellent modular component implementation

This MarkdownEditor component is well-implemented with comprehensive dark mode styling, proper prop handling, and effective dynamic imports. The JSDoc documentation is thorough, and the code correctly handles SSR considerations.

src/hooks/courses/useCourseNavigation.js (1)

1-145: LGTM! Well-structured navigation hook

This hook effectively centralizes course navigation logic with clean state management and responsive behavior. Good use of useMemo for the tab map and proper history manipulation for client-side navigation.

src/hooks/courses/useCourseData.js (1)

1-80: LGTM! Well-implemented data fetching hook

This hook effectively handles course data fetching with proper state management, error handling, and flexible identifier parsing. The code is clean, well-structured, and clearly documented.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented May 12, 2025

✅ Actions performed

Full review triggered.

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

🔭 Outside diff range comments (3)
src/pages/course/[slug]/index.js (3)

58-66: ⚠️ Potential issue

Handle slug being an array to avoid runtime errors

router.query.slug can be either string or string[].
If the user navigates directly to /course/foo/bar, Next JS will supply an array.
Calling .includes or passing the value straight into setNAddress will then throw or produce unexpected behaviour.

-      const { slug } = router.query;
-      if (slug.includes('naddr')) {
-        setNAddress(slug);
+      // Normalise slug to a single string
+      const slugParam = Array.isArray(router.query.slug)
+        ? router.query.slug[0]
+        : router.query.slug;
+
+      if (slugParam?.includes('naddr')) {
+        setNAddress(slugParam);
       } else {

185-190: ⚠️ Potential issue

Optional-chain the method call to prevent “undefined is not a function”

If session.user.purchased is undefined (guest users, new accounts, etc.),
session?.user?.purchased?.some(...) still attempts to invoke .some on
undefined, causing a hard crash.

-    session?.user?.purchased?.some(purchase => purchase.courseId === course?.d);
+    session?.user?.purchased?.some?.(
+      purchase => purchase.courseId === course?.d
+    );

(Adding ?. before the call safely short-circuits to false.)


315-318: 🛠️ Refactor suggestion

Incorrect tab name prevents sidebar from showing on mobile

activeTab === 'lessons' never evaluates to true because the defined tab keys
are 'overview' | 'content' | 'qa'.
As a result, the sidebar is always collapsed on mobile.

-              (isMobileView && activeTab === 'lessons') ? 'ml-0 w-auto opacity-100' : 
+              (isMobileView && activeTab === 'content') ? 'ml-0 w-auto opacity-100' :
♻️ Duplicate comments (1)
src/hooks/encryption/useCourseDecryption.js (1)

24-32: Same course.d identifier issue as in payment hook

Authorization relies on
purchase.courseId === course?.d – verify the key name or reuse a shared getCourseId() helper to avoid drift between hooks.

🧹 Nitpick comments (8)
src/components/content/courses/layout/CourseSidebar.js (1)

40-42: Improved lesson navigation with smooth transitions.

The implementation now uses the onLessonSelect callback instead of forcing a full page reload. This provides a better user experience with smoother transitions between lessons.

Consider adding a comment describing what the onLessonSelect function is expected to do for better documentation, though the existing comment about smooth navigation is helpful.

src/components/content/courses/tabs/CourseQA.js (1)

13-19: Consider extracting relay URLs to a configuration constant.

The hardcoded relay URLs string could be extracted to a configuration constant or file for better maintainability.

+// At the top of the file or in a separate config file
+const RELAY_URLS = "wss://nos.lol/, wss://relay.damus.io/, wss://relay.snort.social/, wss://relay.nostr.band/, wss://relay.primal.net/, wss://nostrue.com/, wss://purplerelay.com/, wss://relay.devs.tools/";

// Then in the component
-            relays="wss://nos.lol/, wss://relay.damus.io/, wss://relay.snort.social/, wss://relay.nostr.band/, wss://relay.primal.net/, wss://nostrue.com/, wss://purplerelay.com/, wss://relay.devs.tools/"
+            relays={RELAY_URLS}
src/components/markdown/MarkdownEditor.js (2)

24-36: Dynamic theme override is fragile

Overwriting mod.default.Markdown at import-time couples us tightly to the library’s internal API; a minor library upgrade could rename/move that field and silently break theming.

Consider:

  1. Leaving the module untouched and applying colours only via CSS variables / global styles, or
  2. Contributing a formal “theme” option upstream and using the public API when it lands.

Both reduce upgrade risk and avoid mutating module state at runtime.


73-123: Global <style> per instance → unnecessary reflow & duplication

<style jsx global> is injected every time MarkdownEditor is mounted, repeating ~50 CSS rules. On pages with multiple editors this:

  • inflates the DOM,
  • forces additional style recalculations,
  • complicates debugging (duplicate selectors).

Move the CSS to a single global stylesheet (e.g. styles/md-editor-dark.css) and import it once in _app.js:

-      <style jsx global>{`/* …lots of rules… */`}</style>
+      {/* global dark-mode styles are imported in _app.js */}

If you need CSS-in-JS, wrap the component with once semantics (e.g. a module-level useEffect with a flag) so the styles are injected only once.

src/hooks/encryption/useCourseDecryption.js (1)

4-4: router parameter is unused

router is accepted but never referenced, triggering an ESLint no-unused-vars warning and confusing future readers.

-const useCourseDecryption = (session, paidCourse, course, lessons, setLessons, router, activeIndex = 0) => {
+const useCourseDecryption = (
+  session,
+  paidCourse,
+  course,
+  lessons,
+  setLessons,
+  activeIndex = 0
+) => {

Drop the param (and update call-sites) or leverage router if you still need to infer the active lesson from the URL.

src/components/content/courses/tabs/CourseOverview.js (1)

1-58: Clean implementation of the CourseOverview component.

This component effectively separates the overview tab content from the main course page, improving code organization. The conditionally rendered elements based on mobile view provide good responsive design.

Some suggestions for further improvement:

  • Consider adding PropTypes validation for stronger type checking
  • The logic for determining if a course is completed might need refinement if a course can be considered complete only when all lessons are completed, not just when any lessons are completed
+import PropTypes from 'prop-types';
 import React from 'react';
 import { Tag } from 'primereact/tag';
 import CourseDetails from '../details/CourseDetails';

 /**
  * Component to display course overview with details
  */
 const CourseOverview = ({ 
   course,
   paidCourse,
   lessons,
   decryptionPerformed,
   handlePaymentSuccess,
   handlePaymentError,
   isMobileView,
   completedLessons 
 }) => {
   // Determine if course is completed
   const isCompleted = completedLessons.length > 0;
   
   return (
     <div className={`bg-gray-800 rounded-lg border border-gray-800 shadow-md ${isMobileView ? 'p-4' : 'p-6'}`}>
       {isMobileView && course && (
         <div className="mb-2">
           {/* Completed tag above image in mobile view */}
           {isCompleted && (
             <div className="mb-2">
               <Tag severity="success" value="Completed" />
             </div>
           )}
           
           {/* Course image */}
           {course.image && (
             <div className="w-full h-48 relative rounded-lg overflow-hidden mb-3">
               <img 
                 src={course.image} 
                 alt={course.title} 
                 className="w-full h-full object-cover"
               />
             </div>
           )}
         </div>
       )}
       <CourseDetails
         processedEvent={course}
         paidCourse={paidCourse}
         lessons={lessons}
         decryptionPerformed={decryptionPerformed}
         handlePaymentSuccess={handlePaymentSuccess}
         handlePaymentError={handlePaymentError}
         isMobileView={isMobileView}
         showCompletedTag={!isMobileView}
       />
     </div>
   );
 };
+
+CourseOverview.propTypes = {
+  course: PropTypes.object,
+  paidCourse: PropTypes.bool,
+  lessons: PropTypes.array,
+  decryptionPerformed: PropTypes.bool,
+  handlePaymentSuccess: PropTypes.func,
+  handlePaymentError: PropTypes.func,
+  isMobileView: PropTypes.bool,
+  completedLessons: PropTypes.array
+};
+
+CourseOverview.defaultProps = {
+  completedLessons: []
+};

 export default CourseOverview; 
src/pages/course/[slug]/index.js (2)

70-86: Defensive access to localStorage for private-window / quota-exceeded cases

localStorage.getItem can throw (e.g. Safari private mode or when quota is
exceeded). Wrap the call in try/catch to keep the page from blanking out for
users in those environments.


220-231: Minor: Memoise getTabItems() and getActiveTabIndex()

Both helpers are recomputed on every render, even though they only depend on
navigation state. Wrapping them in useMemo (inside the custom hook or here)
will avoid unnecessary re-renders of MenuTab.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c73d6eb and 9bd0c08.

📒 Files selected for processing (25)
  • src/components/content/courses/details/CourseDetails.js (1 hunks)
  • src/components/content/courses/layout/CourseSidebar.js (1 hunks)
  • src/components/content/courses/tabs/CourseContent.js (1 hunks)
  • src/components/content/courses/tabs/CourseOverview.js (1 hunks)
  • src/components/content/courses/tabs/CourseQA.js (1 hunks)
  • src/components/forms/combined/CombinedResourceForm.js (2 hunks)
  • src/components/forms/combined/EditDraftCombinedResourceForm.js (2 hunks)
  • src/components/forms/combined/EditPublishedCombinedResourceForm.js (3 hunks)
  • src/components/forms/course/embedded/EmbeddedDocumentForm.js (2 hunks)
  • src/components/forms/document/DocumentForm.js (2 hunks)
  • src/components/forms/document/EditDraftDocumentForm.js (2 hunks)
  • src/components/forms/document/EditPublishedDocumentForm.js (2 hunks)
  • src/components/forms/video/EditPublishedVideoForm.js (2 hunks)
  • src/components/markdown/MarkdownEditor.js (1 hunks)
  • src/components/navbar/Navbar.js (1 hunks)
  • src/config/appConfig.js (1 hunks)
  • src/hooks/courses/index.js (1 hunks)
  • src/hooks/courses/useCourseData.js (1 hunks)
  • src/hooks/courses/useCourseNavigation.js (1 hunks)
  • src/hooks/courses/useCoursePayment.js (1 hunks)
  • src/hooks/courses/useCourseTabs.js (1 hunks)
  • src/hooks/courses/useLessons.js (1 hunks)
  • src/hooks/encryption/useCourseDecryption.js (1 hunks)
  • src/pages/course/[slug]/draft/index.js (1 hunks)
  • src/pages/course/[slug]/index.js (8 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (8)
src/components/forms/document/EditDraftDocumentForm.js (4)
src/components/markdown/MarkdownEditor.js (1)
  • MarkdownEditor (50-126)
src/components/forms/course/embedded/EmbeddedDocumentForm.js (2)
  • content (26-26)
  • handleContentChange (40-42)
src/components/forms/document/DocumentForm.js (2)
  • content (23-23)
  • handleContentChange (37-39)
src/components/forms/document/EditPublishedDocumentForm.js (2)
  • content (31-31)
  • handleContentChange (42-44)
src/components/forms/combined/EditDraftCombinedResourceForm.js (2)
src/components/markdown/MarkdownEditor.js (1)
  • MarkdownEditor (50-126)
src/components/forms/combined/CombinedResourceForm.js (2)
  • content (24-24)
  • handleContentChange (40-42)
src/components/forms/combined/CombinedResourceForm.js (7)
src/components/markdown/MarkdownEditor.js (1)
  • MarkdownEditor (50-126)
src/components/forms/combined/EditDraftCombinedResourceForm.js (2)
  • content (23-23)
  • handleContentChange (79-81)
src/components/forms/course/embedded/EmbeddedDocumentForm.js (2)
  • content (26-26)
  • handleContentChange (40-42)
src/components/forms/document/DocumentForm.js (2)
  • content (23-23)
  • handleContentChange (37-39)
src/components/forms/combined/EditPublishedCombinedResourceForm.js (2)
  • content (32-32)
  • handleContentChange (68-70)
src/components/forms/document/EditPublishedDocumentForm.js (2)
  • content (31-31)
  • handleContentChange (42-44)
src/components/forms/document/EditDraftDocumentForm.js (2)
  • content (23-23)
  • handleContentChange (37-39)
src/components/forms/document/DocumentForm.js (1)
src/components/markdown/MarkdownEditor.js (1)
  • MarkdownEditor (50-126)
src/components/forms/document/EditPublishedDocumentForm.js (1)
src/components/markdown/MarkdownEditor.js (1)
  • MarkdownEditor (50-126)
src/hooks/courses/useCoursePayment.js (2)
src/components/profile/UserAccountLinking.js (1)
  • useSession (24-24)
src/hooks/useToast.js (1)
  • showToast (11-13)
src/hooks/courses/useCourseNavigation.js (10)
src/pages/course/[slug]/index.js (3)
  • useCourseNavigation (43-53)
  • router (28-28)
  • isMobileView (39-39)
src/components/content/courses/details/CourseDetails.js (1)
  • router (38-38)
src/components/content/courses/layout/CourseHeader.js (1)
  • router (12-12)
src/components/content/courses/details/DraftCourseLesson.js (1)
  • router (14-14)
src/components/content/courses/details/DraftCourseDetails.js (1)
  • router (32-32)
src/components/navbar/Navbar.js (1)
  • router (17-17)
src/components/content/courses/lessons/CourseLesson.js (1)
  • isMobileView (24-24)
src/components/content/courses/lessons/DocumentLesson.js (1)
  • isMobileView (24-24)
src/components/content/courses/lessons/CombinedLesson.js (1)
  • isMobileView (29-29)
src/components/content/courses/lessons/VideoLesson.js (1)
  • isMobileView (24-24)
src/hooks/courses/useCourseTabs.js (1)
src/hooks/useWindowWidth.js (1)
  • useWindowWidth (3-24)
🪛 Biome (1.9.4)
src/hooks/courses/useCoursePayment.js

[error] 37-37: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (40)
src/components/navbar/Navbar.js (1)

11-11: Import path updated to reflect component reorganization.

The import path for CourseHeader has been updated to use the new directory structure where course components are organized in a more logical way with layout components in their own subdirectory.

src/pages/course/[slug]/draft/index.js (1)

5-6: Import paths updated to match new directory structure.

The imports for DraftCourseDetails and DraftCourseLesson now correctly reference the new /details/ subdirectory, which aligns with the broader refactoring of course-related components throughout the codebase.

src/components/content/courses/details/CourseDetails.js (1)

22-22: Changed relative import to absolute path.

The import has been updated from a relative path to an absolute path using the @/ alias. This makes the import more consistent with the codebase style and more maintainable if component files get moved.

src/config/appConfig.js (1)

14-15: Added new author public key to authorized users.

A new pubkey has been added to the authorPubkeys array, expanding the list of authorized contributors. This change supports the course authorization and access control mechanisms used throughout the application.

src/components/forms/combined/EditDraftCombinedResourceForm.js (2)

8-14: Improved import organization and standardized markdown editor usage.

The imports have been reorganized in a more logical order, grouping related PrimeReact imports together and adding the new MarkdownEditor component.


243-243: Clean replacement of direct MDEditor with centralized MarkdownEditor component.

The custom MarkdownEditor component now properly encapsulates the dark mode styling and configuration, reducing duplication across forms and standardizing the markdown editing experience.

src/hooks/courses/index.js (1)

1-13: Great use of barrel file pattern for course-related hooks.

This new index file efficiently consolidates all course-related hooks into a single export point, making imports cleaner and more maintainable throughout the application. This pattern improves code organization and makes it easier to identify and use available hooks.

src/components/forms/video/EditPublishedVideoForm.js (2)

3-19: Improved import organization with better PrimeReact styling support.

The imports have been reorganized for better grouping of related components, added required PrimeReact CSS imports, and introduced the standardized MarkdownEditor component.


192-192: Standardized markdown editing with consistent dark mode styling.

Replaced the direct MDEditor usage with the centralized MarkdownEditor component, ensuring consistent styling and behavior across all forms.

src/components/forms/document/EditDraftDocumentForm.js (2)

11-14: Added required CSS imports and standardized markdown editor component.

Added necessary PrimeReact CSS imports and introduced the centralized MarkdownEditor component, aligning with the application-wide standardization effort.


146-146: Simplified markdown editing with consistent styling.

Replaced the direct MDEditor usage with the centralized MarkdownEditor component, ensuring consistent styling and behavior while reducing code duplication.

src/components/forms/document/DocumentForm.js (2)

12-14: Good refactoring: Standardized markdown editor implementation.

The change from a dynamic import of MDEditor to a static import of the new MarkdownEditor component improves code consistency and maintainability. This aligns with similar changes across other form components in the codebase.


148-152: Clean implementation of the new MarkdownEditor component.

The replacement of the inline MDEditor with the MarkdownEditor component maintains the same functionality while leveraging the consistent dark mode styling that's now centralized in the MarkdownEditor component.

src/components/forms/combined/CombinedResourceForm.js (2)

8-14: Import organization improved for better readability.

The imports have been reorganized in a logical manner, with related components grouped together and the new MarkdownEditor component imported from the local path.


200-200: Simplified editor implementation with centralized styling.

The replacement of the wrapped MDEditor with the new MarkdownEditor component reduces markup complexity while maintaining the same functionality, leveraging the centralized dark mode styling.

src/components/forms/course/embedded/EmbeddedDocumentForm.js (2)

16-17: Improved import organization for encryption and markdown editing.

The addition of the useEncryptContent hook import makes the component's dependencies clearer, and the MarkdownEditor import aligns with the standardization across the codebase.


185-185: Standardized markdown editor implementation.

Replacing the MDEditor with MarkdownEditor maintains functionality while benefiting from the centralized styling and configuration, consistent with changes in other form components.

src/components/forms/combined/EditPublishedCombinedResourceForm.js (3)

3-19: Well-organized imports improve code readability.

The import statements have been reorganized in a logical manner, grouping related imports together and adding the new MarkdownEditor component. This maintains consistency with other form components.


222-222: Standardized markdown editor for video embed section.

Replacing the MDEditor with MarkdownEditor for the video embed section maintains functionality while leveraging the centralized styling system, creating a consistent editor experience.


239-239: Standardized markdown editor for content section.

Similarly, the content section now uses the MarkdownEditor component, ensuring consistent styling and behavior across all markdown editing areas of the application.

src/components/forms/document/EditPublishedDocumentForm.js (5)

3-7: Good addition of PrimeReact component imports.

The imports are well-organized and correctly structured, providing the necessary UI components for the form.


10-10: Good implementation of the useToast hook.

The useToast hook is properly imported and used in the component for displaying notifications to users (line 23).


14-14: Proper encryption hook implementation.

The useEncryptContent hook is correctly imported and utilized for encrypting content when it's a paid resource (line 89).


15-18: Good standardization of CSS imports and MarkdownEditor component.

The CSS imports and MarkdownEditor component import are correctly arranged to ensure proper styling and functionality.


201-201: Successful implementation of the custom MarkdownEditor component.

The code effectively replaces the previous markdown editor implementation with the new centralized MarkdownEditor component. This refactoring improves consistency across the application by standardizing the markdown editing experience with proper dark mode styling.

src/hooks/courses/useLessons.js (5)

1-11: Well-documented hook with proper imports and JSDoc.

The hook has clear documentation explaining its purpose, parameters, and return values, making it easy for other developers to understand and use.


12-15: Good state management setup.

The hook correctly initializes state for both the raw lessons array and the deduplicated uniqueLessons array.


17-48: Efficient lesson fetching implementation.

The lesson fetching logic is well-implemented with:

  • Proper dependency array
  • Conditional execution based on lessonIds and pubkey availability
  • Single filter creation to minimize API calls
  • Async/await pattern with try/catch for error handling
  • Author data enrichment for each lesson

This approach is more efficient than fetching lessons individually.


50-56: Good deduplication strategy.

The deduplication logic using Map is an efficient way to ensure unique lessons based on their IDs. This helps prevent duplicate lessons from being displayed in the UI.


58-61: Clean and concise return value.

The hook returns exactly what consuming components need: the full lessons array, deduplicated lessons, and a setter for updating lessons if needed.

src/components/content/courses/tabs/CourseQA.js (2)

1-7: Well-documented component with proper imports.

The component has clear documentation and imports only what's necessary.


7-30: Good conditional rendering based on authorization.

The component correctly displays different content based on user authorization status, providing appropriate feedback to unauthorized users.

src/hooks/courses/useCourseData.js (6)

1-12: Well-structured imports and documentation.

The hook has comprehensive JSDoc documentation explaining its purpose, parameters, and return values.


13-19: Good state management setup.

The hook properly initializes all necessary state variables, including course data, lesson IDs, paid status, and loading state.


20-38: Robust slug and course ID handling.

The implementation correctly handles both plain slugs and NIP-19 encoded addresses, with proper error handling for invalid formats.


39-54: Well-implemented course fetching logic.

The fetchCourse function properly:

  • Connects to NDK
  • Fetches the course event
  • Retrieves author data
  • Extracts lesson IDs from tags
  • Parses and combines course and author data
  • Handles errors appropriately

56-75: Good orchestration of initialization flow.

The initializeCourse function correctly manages the loading state and orchestrates the fetching of course ID and course data, with proper error handling at each step.


77-80: Clean hook API.

The hook returns a concise object with all the necessary data for consuming components.

src/components/content/courses/tabs/CourseContent.js (1)

1-122:

Details

✅ Verification successful

Well-structured component with smooth transitions, but some improvements needed.

This component effectively handles the content tab of the course interface, with good transitions between lessons. The conditional rendering based on lesson type is well-implemented.

Some issues to address:

  1. Nested setTimeout in transition effect could be simplified
  2. Missing PropTypes validation
  3. The default of null for currentLesson could lead to unnecessary re-renders
 import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
 import VideoLesson from '@/components/content/courses/lessons/VideoLesson';
 import DocumentLesson from '@/components/content/courses/lessons/DocumentLesson';
 import CombinedLesson from '@/components/content/courses/lessons/CombinedLesson';
 import MarkdownDisplay from '@/components/markdown/MarkdownDisplay';

 /**
  * Component to display course content including lessons
  */
 const CourseContent = ({ 
   lessons, 
   activeIndex, 
   course, 
   paidCourse, 
   decryptedLessonIds, 
   setCompleted 
 }) => {
   const [lastActiveIndex, setLastActiveIndex] = useState(activeIndex);
   const [isTransitioning, setIsTransitioning] = useState(false);
-  const [currentLesson, setCurrentLesson] = useState(null);
+  const [currentLesson, setCurrentLesson] = useState(() => {
+    return lessons.length > 0 && activeIndex < lessons.length ? lessons[activeIndex] : null;
+  });

   // Initialize current lesson and handle updates when lessons or activeIndex change
   useEffect(() => {
     if (lessons.length > 0 && activeIndex < lessons.length) {
       setCurrentLesson(lessons[activeIndex]);
     } else {
       setCurrentLesson(null);
     }
   }, [lessons, activeIndex]);

   // Handle smooth transitions between lessons
   useEffect(() => {
     if (activeIndex !== lastActiveIndex) {
       // Start transition
       setIsTransitioning(true);
       
-      // After a short delay, update the current lesson
-      const timer = setTimeout(() => {
-        setCurrentLesson(lessons[activeIndex] || null);
-        setLastActiveIndex(activeIndex);
-        
-        // End transition with a slight delay to ensure content is ready
-        setTimeout(() => {
-          setIsTransitioning(false);
-        }, 50);
-      }, 300); // Match this with CSS transition duration
+      // Set a single timer that completes the entire transition
+      const transitionDuration = 300; // Match this with CSS transition duration
+      const timer = setTimeout(() => {
+        setCurrentLesson(lessons[activeIndex] || null);
+        setLastActiveIndex(activeIndex);
+        setIsTransitioning(false);
+      }, transitionDuration);
       
       return () => clearTimeout(timer);
     }
   }, [activeIndex, lastActiveIndex, lessons]);

   const renderLesson = (lesson) => {
     if (!lesson) return null;
     
     // Check if this specific lesson is decrypted
     const lessonDecrypted = !paidCourse || decryptedLessonIds[lesson.id] || false;
     
     if (lesson.topics?.includes('video') && lesson.topics?.includes('document')) {
       return (
         <CombinedLesson
           key={`combined-${lesson.id}`}
           lesson={lesson}
           course={course}
           decryptionPerformed={lessonDecrypted}
           isPaid={paidCourse}
           setCompleted={setCompleted}
         />
       );
     } else if (lesson.type === 'video' || lesson.topics?.includes('video')) {
       return (
         <VideoLesson
           key={`video-${lesson.id}`}
           lesson={lesson}
           course={course}
           decryptionPerformed={lessonDecrypted}
           isPaid={paidCourse}
           setCompleted={setCompleted}
         />
       );
     } else if (lesson.type === 'document' || lesson.topics?.includes('document')) {
       return (
         <DocumentLesson
           key={`doc-${lesson.id}`}
           lesson={lesson}
           course={course}
           decryptionPerformed={lessonDecrypted}
           isPaid={paidCourse}
           setCompleted={setCompleted}
         />
       );
     }
     
     return null;
   };

   return (
     <>
       {lessons.length > 0 && currentLesson ? (
         <div className="bg-gray-800 rounded-lg shadow-sm overflow-hidden">
           <div 
             key={`lesson-container-${currentLesson.id}`}
             className={`transition-opacity duration-300 ease-in-out ${isTransitioning ? 'opacity-0' : 'opacity-100'}`}
           >
             {renderLesson(currentLesson)}
           </div>
         </div>
       ) : (
         <div className={`text-center bg-gray-800 rounded-lg p-8 transition-opacity duration-300 ease-in-out ${isTransitioning ? 'opacity-0' : 'opacity-100'}`}>
           <p>Select a lesson from the sidebar to begin learning.</p>
         </div>
       )}

       {course?.content && (
         <div className="mt-8 bg-gray-800 rounded-lg shadow-sm">
           <MarkdownDisplay content={course.content} className="p-4 rounded-lg" />
         </div>
       )}
     </>
   );
 };
+
+CourseContent.propTypes = {
+  lessons: PropTypes.array.isRequired,
+  activeIndex: PropTypes.number.isRequired,
+  course: PropTypes.object.isRequired,
+  paidCourse: PropTypes.bool,
+  decryptedLessonIds: PropTypes.object,
+  setCompleted: PropTypes.func
+};
+
+CourseContent.defaultProps = {
+  paidCourse: false,
+  decryptedLessonIds: {},
+  setCompleted: () => {}
+};

 export default CourseContent; 

Let's check if the transition timing matches CSS classes:


🏁 Script executed:

#!/bin/bash
# Check for CSS transitions in the styling files
rg "transition-opacity" --include="*.css" --include="*.scss" src/
rg "duration-300" --include="*.css" --include="*.scss" src/

Length of output: 1009


All improvements verified – no further changes needed

The transition timing of 300 ms aligns with the Tailwind CSS utility class duration-300, so the simplified single setTimeout covers both the opacity transition and lesson update. PropTypes and defaultProps have been correctly added, and initializing currentLesson via a lazy state initializer prevents extra renders.

No outstanding issues remain.

src/pages/course/[slug]/index.js (1)

142-154: Guard activeIndex to avoid out-of-bounds look-ups

When uniqueLessons updates (e.g. after decryption finishes) its length can
momentarily be smaller than the previous activeIndex.
uniqueLessons[activeIndex] will then be undefined, causing an attempt to
read .id on undefined two lines later.

Add an early return:

-    if (paidCourse && uniqueLessons.length > 0) {
-      const currentLesson = uniqueLessons[activeIndex];
+    if (
+      paidCourse &&
+      uniqueLessons.length > 0 &&
+      activeIndex < uniqueLessons.length
+    ) {
+      const currentLesson = uniqueLessons[activeIndex];

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

🔭 Outside diff range comments (1)
src/components/forms/course/embedded/EmbeddedDocumentForm.js (1)

170-186: ⚠️ Potential issue

Missing Tooltip component for info icon.

There's an info icon with tooltip attributes (lines 190-197), but the Tooltip component that was likely removed during refactoring isn't being rendered. This may result in the tooltip not working properly.

Add the Tooltip component at the end of the form to ensure tooltips work correctly:

      <div className="p-inputgroup flex-1 flex-col mt-4">
        <span>Content</span>
        <MarkdownEditor value={content} onChange={handleContentChange} height={350} />
      </div>
+      <Tooltip target=".pi-info-circle" />
♻️ Duplicate comments (4)
src/components/content/courses/tabs/CourseOverview.js (1)

19-19: Revisit course completion logic

The current implementation considers a course completed if there's at least one completed lesson, which might not accurately reflect full course completion.

Consider implementing a more robust completion check:

-  const isCompleted = completedLessons.length > 0;
+  const isCompleted = lessons && lessons.length > 0 && completedLessons.length === lessons.length;

This ensures a course is only marked as completed when all lessons have been completed.

src/hooks/encryption/useCourseDecryption.js (2)

24-32: ⚠️ Potential issue

course?.d typo still blocks access control
The property should be course?.id; otherwise hasAccess is always false, preventing paying users from seeing their content.

-      session.user.purchased?.some(purchase => purchase.courseId === course?.d) ||
+      session.user.purchased?.some(purchase => purchase.courseId === course?.id) ||

70-98: 🛠️ Refactor suggestion

Timers are not cleared → memory leak & setState-after-unmount risk

Both the 10-second timeoutId and the 5-second retry timer live beyond the component’s lifecycle.
If the user leaves the page early they will still fire, potentially calling setLoading or setLessons on an unmounted component.

+  const decryptTimeoutRef = useRef(null);
+  const retryTimeoutRef   = useRef(null);
...
-      const timeoutPromise = new Promise((_, reject) => {
-        timeoutId = setTimeout(() => {
+      const timeoutPromise = new Promise((_, reject) => {
+        timeoutId = setTimeout(() => {
           ...
-        }, 10000);
+        }, 10000);
       });
+      decryptTimeoutRef.current = timeoutId;
...
-        setTimeout(() => {
+        retryTimeoutRef.current = setTimeout(() => {
           processingRef.current = false;
           decryptCurrentLesson();
-        }, 5000);
+        }, 5000);
...
-  }, [currentLesson, hasAccess, paidCourse, decryptContent, lessons, setLessons, decryptedLessonIds]);
+  }, [currentLesson, hasAccess, paidCourse, decryptContent, lessons, setLessons, decryptedLessonIds]);
+
+  // Cleanup all pending timers on unmount
+  useEffect(() => () => {
+    clearTimeout(decryptTimeoutRef.current);
+    clearTimeout(retryTimeoutRef.current);
+  }, []);
src/pages/course/[slug]/index.js (1)

37-153: isDecrypting is derived state – duplicate of earlier feedback
Storing it in useState + useEffect adds an unnecessary render cycle and risk of stale state. You can compute the boolean with useMemo directly from dependencies.

(See previous review on older commit; recommendation still applies.)

🧹 Nitpick comments (17)
src/components/markdown/MarkdownEditor.js (1)

73-123: Consider extracting the CSS to a separate stylesheet

While the global styles effectively ensure consistent dark mode rendering, consider extracting these styles to a separate CSS file to improve maintainability and reduce the JSX complexity. The extensive use of !important flags, while necessary here, would be better isolated in a dedicated stylesheet.

-      <style jsx global>{`
-        /* Force all text to white in editor */
-        .w-md-editor * {
-          color: white !important;
-        }
-        
-        /* Reset preview text color */
-        .w-md-editor-preview * {
-          color: #c9d1d9 !important;
-        }
-        
-        /* Editor backgrounds */
-        .md-editor-dark {
-          background-color: #0d1117 !important;
-        }
-        
-        .w-md-editor-text-input {
-          caret-color: white !important;
-          -webkit-text-fill-color: white !important;
-          color: white !important;
-        }
-        
-        .w-md-editor-toolbar {
-          background-color: #161b22 !important;
-          border-bottom: 1px solid #30363d !important;
-        }
-        
-        /* Preview styling */
-        .w-md-editor-preview {
-          background-color: #0d1117 !important;
-          font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
-        }
-        
-        /* Make code blocks maintain their styling */
-        .w-md-editor-preview pre {
-          background-color: #1e1e1e !important;
-          color: #d4d4d4 !important;
-          padding: 1em !important;
-          border-radius: 5px !important;
-        }
-        
-        .w-md-editor-preview code {
-          font-family: 'Consolas', 'Monaco', 'Andale Mono', 'Ubuntu Mono', monospace !important;
-          color: #d4d4d4 !important;
-        }
-        
-        /* Force anything with text-rendering to be white */
-        [style*="text-rendering"] {
-          color: white !important;
-        }
-      `}</style>

Then import a stylesheet like:

// At the top of the file
import '@/styles/markdown-editor-dark.css';
src/components/forms/video/EditPublishedVideoForm.js (1)

44-46: Remove debugging console.log statement

There's an unused console.log statement that should be removed from the production code.

-  useEffect(() => {
-    console.log('event', event);
-  }, [event]);
src/components/content/courses/details/CourseDetails.js (1)

22-22: Consider consistent import style

The import for DesktopCourseDetails has been updated to use an absolute path with the @/ alias, which is good for maintainability. However, the import for MobileCourseDetails on line 23 still uses a relative path. For consistency, consider updating all imports to use the same style.

-import MobileCourseDetails from './MobileCourseDetails';
+import MobileCourseDetails from '@/components/content/courses/details/MobileCourseDetails';
src/components/forms/course/embedded/EmbeddedDocumentForm.js (1)

2-17: Some imports may be unused.

Several new imports have been added (axios, InputTextarea, Calendar, useRouter), but they don't appear to be used in the visible code. Consider cleaning up any unused imports to prevent code bloat.

-import axios from 'axios';
-import { InputTextarea } from 'primereact/inputtextarea';
-import { Calendar } from 'primereact/calendar';
-import { useRouter } from 'next/router';
src/components/content/courses/tabs/CourseQA.js (1)

16-16: Consider extracting relay list to a configuration file.

The hardcoded list of relay URLs could be moved to a central configuration file, making it easier to maintain and update across the application.

// In a config file like src/config/nostr.js
export const DEFAULT_RELAYS = "wss://nos.lol/, wss://relay.damus.io/, wss://relay.snort.social/, wss://relay.nostr.band/, wss://relay.primal.net/, wss://nostrue.com/, wss://purplerelay.com/, wss://relay.devs.tools/";

// Then in this component:
import { DEFAULT_RELAYS } from '@/config/nostr';

// And use it like:
relays={DEFAULT_RELAYS}
src/hooks/courses/useLessons.js (1)

1-61: Well-structured hook with efficient implementation

This new hook effectively modularizes lesson fetching and management with a clean separation of concerns. The single filter approach to fetch all lessons in one network call is an optimization worth highlighting.

I particularly like:

  • The comprehensive error handling in the fetch operation
  • Using a Map for efficient lesson deduplication
  • Clear JSDoc documentation

Consider adding an isLoading state to indicate when lessons are being fetched:

const useLessons = (ndk, fetchAuthor, lessonIds, pubkey) => {
  const [lessons, setLessons] = useState([]);
  const [uniqueLessons, setUniqueLessons] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
  
  // Fetch lessons when IDs or pubkey change
  useEffect(() => {
    if (lessonIds.length > 0 && pubkey) {
      const fetchLessons = async () => {
+       setIsLoading(true);
        try {
          await ndk.connect();
          
          // Rest of the function...
          
          setLessons(newLessons);
+         setIsLoading(false);
        } catch (error) {
          console.error('Error fetching events:', error);
+         setIsLoading(false);
        }
      };
      
      fetchLessons();
    }
  }, [lessonIds, ndk, fetchAuthor, pubkey]);

  // Rest of the hook...

- return { lessons, uniqueLessons, setLessons };
+ return { lessons, uniqueLessons, setLessons, isLoading };
};

This would allow components consuming this hook to show loading states to users.

src/hooks/courses/useCoursePayment.js (1)

37-37: Simplify conditional check with optional chaining

There's a redundant condition check in this line.

- if (response && response?.preimage) {
+ if (response?.preimage) {

The optional chaining operator ?. already handles the case where response is null or undefined, making the response && check unnecessary.

🧰 Tools
🪛 Biome (1.9.4)

[error] 37-37: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/hooks/courses/useCourseNavigation.js (1)

14-23: Consider extracting the tabMap logic for testability

This memoized function is well-implemented, but could be extracted for better testability.

+ // Helper function to get tabMap based on view mode
+ const getTabMap = (isMobileView) => {
+   const baseTabMap = ['overview', 'content', 'qa'];
+   if (isMobileView) {
+     const mobileTabMap = [...baseTabMap];
+     mobileTabMap.splice(2, 0, 'lessons');
+     return mobileTabMap;
+   }
+   return baseTabMap;
+ };

const useCourseNavigation = (router, isMobileView) => {
  // ...existing state declarations

  // Memoized function to get the tab map based on view mode
  const tabMap = useMemo(() => {
-    const baseTabMap = ['overview', 'content', 'qa'];
-    if (isMobileView) {
-      const mobileTabMap = [...baseTabMap];
-      mobileTabMap.splice(2, 0, 'lessons');
-      return mobileTabMap;
-    }
-    return baseTabMap;
+    return getTabMap(isMobileView);
  }, [isMobileView]);

  // ...rest of the hook
}

// Export the helper for testing
export { getTabMap };
export default useCourseNavigation;

This makes the function independently testable and clarifies its responsibility.

src/hooks/courses/useCourseData.js (1)

67-70: Guarantee a boolean in paidCourse

parsedCourse.price && parsedCourse.price > 0 yields undefined when price is 0.
Consumers expecting a boolean may mis-behave.

-        setPaidCourse(parsedCourse.price && parsedCourse.price > 0);
+        setPaidCourse(Boolean(parsedCourse?.price) && Number(parsedCourse.price) > 0);
src/hooks/encryption/useCourseDecryption.js (1)

55-60: Retry counter increments before passing the threshold

Currently the retry count is increased before the >= MAX_RETRIES check, which effectively allows only MAX_RETRIES – 1 real attempts.

-    // Limit maximum retries
-    if (retryCountRef.current[currentLesson.id] >= MAX_RETRIES) {
-      return;
-    }
-    
-    // Increment retry count
-    retryCountRef.current[currentLesson.id]++;
+    // Limit maximum retries
+    if (retryCountRef.current[currentLesson.id] >= MAX_RETRIES) {
+      return;
+    }
+
+    // Increment retry count *after* the guard
+    retryCountRef.current[currentLesson.id] += 1;
src/components/content/courses/tabs/CourseContent.js (2)

37-49: Second timeout not cleared & potential state update after unmount

Only the outer timer is cleared; the nested 50 ms timeout isn’t.
Add a ref to store the inner id and clear both in the cleanup to avoid subtle memory leaks.

-      const timer = setTimeout(() => {
+      const timer = setTimeout(() => {
         ...
-        setTimeout(() => {
+        const inner = setTimeout(() => {
           setIsTransitioning(false);
-        }, 50);
+        }, 50);
+        // store so we can cancel on unmount
+        innerTimeoutRef.current = inner;
       }, 300);
 
-      return () => clearTimeout(timer);
+      return () => {
+        clearTimeout(timer);
+        clearTimeout(innerTimeoutRef.current);
+      };

56-57: Boolean coercion can be simplified

const lessonDecrypted = !paidCourse || decryptedLessonIds[lesson.id] || false;
The trailing || false is redundant—!paidCourse already yields a boolean and the second term is either undefined or true.

-    const lessonDecrypted = !paidCourse || decryptedLessonIds[lesson.id] || false;
+    const lessonDecrypted = !paidCourse || Boolean(decryptedLessonIds[lesson.id]);
src/hooks/courses/useCourseTabs.js (3)

38-52: Effect re-runs on every navigation because router.query is a mutable object

router.query is a new object on each render, so it constantly invalidates this useEffect, even if the query params have not changed.
Destructure only the value(s) you care about to stabilise the dependency array:

-useEffect(() => {
-  if (router.isReady) {
-    const { active } = router.query;
+const { isReady, query: { active } = {} } = router;
+useEffect(() => {
+  if (isReady) {

60-71: Validate requested tab names to avoid out-of-bounds errors

If indexOrName is a string not present in tabMap, tabName will be undefined, breaking downstream logic.
Consider an early return or default:

-const tabName = typeof indexOrName === 'number'
-  ? tabMap[indexOrName]
-  : indexOrName;
+const tabName =
+  typeof indexOrName === 'number'
+    ? tabMap[indexOrName]
+    : indexOrName;
+if (!tabMap.includes(tabName)) return; // ignore bad input

80-105: Couple getTabItems to tabMap instead of hard-coding

If consumers supply a custom options.tabMap, getTabItems still returns hard-coded items, causing index mismatches.
Generate labels/icons directly from tabMap (with a lookup table) to keep both arrays in sync.

src/pages/course/[slug]/index.js (2)

71-86: Reset corrupted localStorage entries

If JSON.parse fails we silently ignore the error but keep the stale/corrupt value in storage. Consider wiping the key so the user can recover automatically:

} catch (error) {
  console.error('Error parsing completed lessons from storage:', error);
+  localStorage.removeItem(storageKey);
+  setCompletedLessons([]);
}

190-190: Null-safe access to session.user.purchased

session.user.purchased may be undefined; the current optional-chain prevents a crash, but the filter runs on an undefined array, returning undefined not false. Wrap with ?.some ?? false to ensure a boolean.

-    session?.user?.purchased?.some(purchase => purchase.courseId === course?.d);
+    session?.user?.purchased?.some(p => p.courseId === course?.d) ?? false;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c73d6eb and 9bd0c08.

📒 Files selected for processing (25)
  • src/components/content/courses/details/CourseDetails.js (1 hunks)
  • src/components/content/courses/layout/CourseSidebar.js (1 hunks)
  • src/components/content/courses/tabs/CourseContent.js (1 hunks)
  • src/components/content/courses/tabs/CourseOverview.js (1 hunks)
  • src/components/content/courses/tabs/CourseQA.js (1 hunks)
  • src/components/forms/combined/CombinedResourceForm.js (2 hunks)
  • src/components/forms/combined/EditDraftCombinedResourceForm.js (2 hunks)
  • src/components/forms/combined/EditPublishedCombinedResourceForm.js (3 hunks)
  • src/components/forms/course/embedded/EmbeddedDocumentForm.js (2 hunks)
  • src/components/forms/document/DocumentForm.js (2 hunks)
  • src/components/forms/document/EditDraftDocumentForm.js (2 hunks)
  • src/components/forms/document/EditPublishedDocumentForm.js (2 hunks)
  • src/components/forms/video/EditPublishedVideoForm.js (2 hunks)
  • src/components/markdown/MarkdownEditor.js (1 hunks)
  • src/components/navbar/Navbar.js (1 hunks)
  • src/config/appConfig.js (1 hunks)
  • src/hooks/courses/index.js (1 hunks)
  • src/hooks/courses/useCourseData.js (1 hunks)
  • src/hooks/courses/useCourseNavigation.js (1 hunks)
  • src/hooks/courses/useCoursePayment.js (1 hunks)
  • src/hooks/courses/useCourseTabs.js (1 hunks)
  • src/hooks/courses/useLessons.js (1 hunks)
  • src/hooks/encryption/useCourseDecryption.js (1 hunks)
  • src/pages/course/[slug]/draft/index.js (1 hunks)
  • src/pages/course/[slug]/index.js (8 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (9)
src/components/forms/combined/EditDraftCombinedResourceForm.js (7)
src/components/markdown/MarkdownEditor.js (1)
  • MarkdownEditor (50-126)
src/components/forms/combined/CombinedResourceForm.js (2)
  • content (24-24)
  • handleContentChange (40-42)
src/components/forms/combined/EditPublishedCombinedResourceForm.js (2)
  • content (32-32)
  • handleContentChange (68-70)
src/components/forms/course/embedded/EmbeddedDocumentForm.js (2)
  • content (26-26)
  • handleContentChange (40-42)
src/components/forms/document/DocumentForm.js (2)
  • content (23-23)
  • handleContentChange (37-39)
src/components/forms/document/EditPublishedDocumentForm.js (2)
  • content (31-31)
  • handleContentChange (42-44)
src/components/forms/document/EditDraftDocumentForm.js (2)
  • content (23-23)
  • handleContentChange (37-39)
src/components/forms/combined/CombinedResourceForm.js (1)
src/components/markdown/MarkdownEditor.js (1)
  • MarkdownEditor (50-126)
src/components/forms/document/DocumentForm.js (1)
src/components/markdown/MarkdownEditor.js (1)
  • MarkdownEditor (50-126)
src/components/forms/document/EditPublishedDocumentForm.js (1)
src/components/markdown/MarkdownEditor.js (1)
  • MarkdownEditor (50-126)
src/components/content/courses/tabs/CourseOverview.js (3)
src/pages/course/[slug]/index.js (4)
  • completedLessons (32-32)
  • isMobileView (39-39)
  • handlePaymentSuccess (191-198)
  • handlePaymentError (200-206)
src/components/content/courses/lessons/DocumentLesson.js (1)
  • isMobileView (24-24)
src/components/content/courses/details/CourseDetails.js (1)
  • CourseDetails (25-233)
src/components/forms/video/EditPublishedVideoForm.js (2)
src/components/markdown/MarkdownEditor.js (1)
  • MarkdownEditor (50-126)
src/components/forms/combined/EditPublishedCombinedResourceForm.js (2)
  • videoEmbed (31-31)
  • handleVideoEmbedChange (64-66)
src/hooks/courses/useLessons.js (5)
src/pages/course/[slug]/index.js (2)
  • useLessons (125-130)
  • fetchAuthor (108-116)
src/components/content/courses/details/CourseDetails.js (2)
  • fetchAuthor (88-99)
  • author (36-36)
src/components/content/courses/details/DraftCourseDetails.js (2)
  • fetchAuthor (35-46)
  • author (23-23)
src/pages/course/[slug]/draft/index.js (2)
  • fetchAuthor (31-43)
  • lessons (15-15)
src/hooks/courses/useCourseData.js (1)
  • lessonIds (15-15)
src/hooks/courses/useCoursePayment.js (2)
src/components/profile/UserAccountLinking.js (1)
  • useSession (24-24)
src/hooks/useToast.js (1)
  • showToast (11-13)
src/hooks/courses/useCourseTabs.js (1)
src/hooks/useWindowWidth.js (1)
  • useWindowWidth (3-24)
🪛 Biome (1.9.4)
src/hooks/courses/useCoursePayment.js

[error] 37-37: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (35)
src/config/appConfig.js (1)

14-15: LGTM: New author public key added

The addition of a new public key to the authorPubkeys array aligns with the course refactoring initiative, enabling proper authorization for the new course management functionality.

src/components/markdown/MarkdownEditor.js (3)

1-37: Well-implemented dynamic import with theme overriding

The dynamic import approach is correctly implemented with SSR disabled for client-side rendering of the markdown editor. The theme overriding is elegantly handled by extending the module's default theme properties.


38-58: Well-documented component with comprehensive props

The component is thoroughly documented with JSDoc comments, clearly describing all available props with their types and default values. The prop structure offers good flexibility with sensible defaults.


59-72: Clean implementation with proper prop forwarding

The component efficiently wraps the MDEditor with dark mode styling and properly forwards all relevant props, including the spread operator for additional props, enhancing reusability.

src/components/forms/combined/EditDraftCombinedResourceForm.js (2)

8-14: Good import organization and new MarkdownEditor component

The imports have been well organized, with the custom MarkdownEditor component now properly imported. This is part of the broader course refactoring initiative to standardize the markdown editing experience.


243-243: Cleaner implementation with the new MarkdownEditor component

The previous implementation likely used a direct import of the markdown editor with a wrapping div. This change simplifies the JSX and leverages the new reusable component, which is a good refactoring practice.

src/components/forms/document/EditDraftDocumentForm.js (2)

11-14: LGTM: Proper CSS imports and MarkdownEditor component

The addition of the necessary CSS imports and the new MarkdownEditor component aligns with the standardization effort across form components in the codebase.


146-146: Simplified markdown editor implementation

The change to use the new MarkdownEditor component reduces boilerplate code and ensures a consistent styling and behavior across all forms in the application.

src/components/forms/document/DocumentForm.js (2)

12-12: Import standardization to use local MarkdownEditor component

The code now imports the local MarkdownEditor component instead of dynamically importing the external MDEditor. This change helps standardize the markdown editing experience across the application.

Also applies to: 14-14


148-152: Clean implementation using the central MarkdownEditor component

Replaced the direct usage of MDEditor with the wrapper div to the centralized MarkdownEditor component, which encapsulates the dark mode styling and consistent editor appearance.

src/components/forms/document/EditPublishedDocumentForm.js (3)

3-7: Improved organization of PrimeReact component imports

Imports are now grouped more logically with all PrimeReact UI components at the top, improving code readability.


10-18: Added explicit CSS imports and organized custom hooks imports

Added explicit PrimeReact CSS imports and reordered hook imports for better organization. This ensures all necessary styles are available and improves code maintainability.


201-201: Simplified markdown editor implementation with MarkdownEditor component

Replaced the complex MDEditor implementation with the centralized MarkdownEditor component, removing the need for wrapper divs with data-color-mode attributes.

src/components/forms/video/EditPublishedVideoForm.js (3)

3-7: Reorganized PrimeReact UI component imports

UI component imports are now grouped together at the top of the file for better organization and readability.


15-19: Added explicit stylesheets and custom component imports

Added explicit imports for PrimeReact and PrimeIcons CSS, and the custom MarkdownEditor component, ensuring proper styling and consistent components usage.


192-192: Standardized video embed editing with MarkdownEditor component

Replaced the MDEditor implementation with the centralized MarkdownEditor component, maintaining the same functionality while using a consistent UI component.

src/components/navbar/Navbar.js (1)

11-11: Updated import path following course components restructuring

The import path for CourseHeader was updated to reflect the new directory structure, where course components are now organized into logical subdirectories (layout, tabs, etc.), improving code organization.

src/pages/course/[slug]/draft/index.js (1)

5-6: Import paths updated for better organization

The import paths for DraftCourseDetails and DraftCourseLesson have been updated to reflect the new directory structure, moving them into a dedicated details/ subdirectory. This change aligns with the broader refactoring effort to improve modularity in the course components.

src/components/content/courses/layout/CourseSidebar.js (1)

39-42: Improved navigation with client-side routing

Great improvement! The navigation logic has been updated to use client-side routing through the onLessonSelect callback instead of forcing a full page reload with window.location.href. This will provide a smoother user experience when navigating between lessons.

src/hooks/courses/index.js (1)

1-13: Well-organized hook exports

This new index file creates a centralized location for all course-related hooks, following a clean pattern for modular code organization. By re-exporting these hooks from a single file, you've made imports cleaner throughout the application and improved the maintainability of the codebase.

src/components/forms/combined/CombinedResourceForm.js (2)

8-14: Import reorganization improves code structure.

The imports have been logically regrouped, and the markdown editor implementation has been standardized by replacing the external @uiw/react-md-editor with a local custom component.


200-200: Good refactoring of markdown editor integration.

The markdown editor implementation has been successfully replaced with the new standardized MarkdownEditor component, which encapsulates the dark mode styling and configuration internally, making the code more maintainable.

src/components/forms/course/embedded/EmbeddedDocumentForm.js (1)

185-185: Consistent implementation of markdown editor.

The markdown editor has been standardized with the custom MarkdownEditor component, which is a good refactoring that maintains functionality while providing consistent styling across the application.

src/components/forms/combined/EditPublishedCombinedResourceForm.js (2)

3-19: Improved import organization.

The imports have been properly organized by grouping related PrimeReact components and styles together, which enhances readability and maintainability.


222-222: Consistent markdown editor implementation in both fields.

Both the video embed editor and content editor now use the standardized MarkdownEditor component, ensuring consistent user experience and simplifying the codebase by removing duplicate styling logic.

Also applies to: 239-239

src/components/content/courses/tabs/CourseQA.js (1)

1-32: Well-structured new component for course Q&A.

This new component properly separates concerns by encapsulating the course comments functionality. The conditional rendering based on authorization status provides clear user feedback. The component is well-documented with JSDoc comments.

src/components/content/courses/tabs/CourseOverview.js (1)

21-56: Good responsive design pattern with conditional rendering

The component effectively handles different layouts for mobile and desktop views, with appropriate conditional rendering for the image and completion tag.

src/hooks/courses/useCoursePayment.js (2)

19-33: Comprehensive authorization logic

The authorization logic thoroughly covers all relevant scenarios:

  • Subscription status
  • Course creator check
  • Course price check
  • Individual course purchase verification

58-65: Nice API with complete return values

The hook returns all necessary values for components to handle payment flows and authorization, creating a clean separation between data/logic and presentation.

src/hooks/courses/useCourseNavigation.js (2)

44-57: Client-side navigation improvement with history API

The handleLessonSelect function smartly updates the URL without causing a page reload using the History API, which is great for maintaining state and improving user experience.


109-127: Excellent keyboard navigation implementation

The keyboard navigation with left/right arrow keys to switch tabs is a nice accessibility feature. The circular navigation (wrapping from last to first tab and vice versa) provides a smooth user experience.

src/hooks/courses/useCourseData.js (2)

40-54: Avoid repeated ndk.connect() calls & strengthen tag parsing

Inside fetchCourse the hook reconnects to NDK every time the effect runs.
That can be dozens of times on route changes or when any dependency mutates, adding needless latency.

-        await ndk.connect();
+        // Connect only once – NDK silently ignores subsequent calls
+        if (!ndk.connected) {
+          await ndk.connect();
+        }

Also, tag[1].split(':')[2] assumes a strict 3-part format. Add a length check to dodge TypeError: Cannot read property '2' of undefined.

-        const lessonIds = event.tags.filter(tag => tag[0] === 'a').map(tag => tag[1].split(':')[2]);
+        const lessonIds = event.tags
+          .filter(tag => tag[0] === 'a' && typeof tag[1] === 'string')
+          .map(tag => {
+            const parts = tag[1].split(':');
+            return parts.length === 3 ? parts[2] : null;
+          })
+          .filter(Boolean);

[ suggest_essential_refactor ]


74-75: Effect churn caused by unstable deps

router.query, ndk, and showToast are new object/function references every render, so the effect constantly re-fires.
Consider wrapping showToast in useCallback inside its provider and exposing a memoised ndk instance (or omitting it from the dependency array if it’s globally stable).

src/components/content/courses/tabs/CourseContent.js (1)

24-29: Guard against out-of-range activeIndex

If activeIndex is larger than lessons.length - 1, currentLesson is set to null but lastActiveIndex keeps the old value, so subsequent transitions won’t trigger.
Consider clamping activeIndex or resetting lastActiveIndex together.

src/hooks/courses/useCourseTabs.js (1)

108-125: Avoid multiple global key-down listeners

If the hook is mounted more than once, each instance adds another keydown listener.
Track a ref counter or add the listener only once at module scope to prevent duplicate handlers.

AustinKelsay and others added 3 commits May 12, 2025 09:29
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
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: 0

♻️ Duplicate comments (3)
src/hooks/courses/useCourseNavigation.js (1)

47-59: ⚠️ Potential issue

Replace direct history manipulation with Next.js Router API.

The function uses direct DOM manipulation with window.history.replaceState, which bypasses Next.js's router lifecycle. This was mentioned in a previous review but not addressed.

Replace with Next.js's built-in shallow routing:

- const newUrl = `/course/${router.query.slug}?active=${index}`;
- window.history.replaceState({ url: newUrl, as: newUrl, options: { shallow: true } }, '', newUrl);
+ router.push(
+   {
+     pathname: `/course/${router.query.slug}`,
+     query: { active: index }
+   },
+   undefined,
+   { shallow: true }
+ );
src/hooks/encryption/useCourseDecryption.js (1)

25-34: ⚠️ Potential issue

Wrong property name breaks access-control logic.

course?.d on line 30 is almost certainly a typo for course?.id. Due to this error, hasAccess will always return false for users who purchased the course, blocking decryption and showing locked content.

-      session.user.purchased?.some(purchase => purchase.courseId === course?.d) ||
+      session.user.purchased?.some(purchase => purchase.courseId === course?.id) ||
src/pages/course/[slug]/index.js (1)

143-154: 🛠️ Refactor suggestion

Consider replacing this useEffect with useMemo for derived state

This is a classic example of derived state. The isDecrypting state depends entirely on other state values and is recalculated whenever any of its dependencies change. As suggested in the past review comments, this could be simplified using useMemo.

-  const [isDecrypting, setIsDecrypting] = useState(false);
+  const isDecrypting = React.useMemo(() => {
+    if (!paidCourse || uniqueLessons.length === 0) return false;
+    const currentLesson = uniqueLessons[activeIndex];
+    return currentLesson && !decryptedLessonIds[currentLesson.id];
+  }, [activeIndex, uniqueLessons, decryptedLessonIds, paidCourse]);

-  useEffect(() => {
-    if (paidCourse && uniqueLessons.length > 0) {
-      const currentLesson = uniqueLessons[activeIndex];
-      if (currentLesson && !decryptedLessonIds[currentLesson.id]) {
-        setIsDecrypting(true);
-      } else {
-        setIsDecrypting(false);
-      }
-    } else {
-      setIsDecrypting(false);
-    }
-  }, [activeIndex, uniqueLessons, decryptedLessonIds, paidCourse]);

This approach:

  1. Eliminates an extra render cycle
  2. Removes the risk of race conditions
  3. Makes the code more declarative
🧹 Nitpick comments (1)
src/hooks/courses/useCourseTabsState.js (1)

107-125: Consider adding a dependency on document for the keyboard navigation effect

The keyboard navigation effect attaches event listeners to the document, but doesn't include it in the dependencies array. While document is unlikely to change, explicitly including it follows the React hooks exhaustive deps rule.

  }, [getActiveTabIndex, tabMap, toggleTab]);
+  }, [getActiveTabIndex, tabMap, toggleTab, typeof document !== 'undefined' ? document : undefined]);

Alternatively, you could conditionally run the effect only in browser environments:

  useEffect(() => {
+   if (typeof document === 'undefined') return;
    
    const handleKeyDown = (e) => {
      // ...
    };

    document.addEventListener('keydown', handleKeyDown);
    return () => {
      document.removeEventListener('keydown', handleKeyDown);
    };
-  }, [getActiveTabIndex, tabMap, toggleTab]);
+  }, [getActiveTabIndex, tabMap, toggleTab]);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9bd0c08 and ccda05d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (11)
  • package.json (1 hunks)
  • src/components/content/courses/tabs/CourseOverview.js (1 hunks)
  • src/components/forms/course/embedded/EmbeddedDocumentForm.js (2 hunks)
  • src/hooks/courses/index.js (1 hunks)
  • src/hooks/courses/useCourseData.js (1 hunks)
  • src/hooks/courses/useCourseNavigation.js (1 hunks)
  • src/hooks/courses/useCoursePayment.js (1 hunks)
  • src/hooks/courses/useCourseTabs.js (1 hunks)
  • src/hooks/courses/useCourseTabsState.js (1 hunks)
  • src/hooks/encryption/useCourseDecryption.js (1 hunks)
  • src/pages/course/[slug]/index.js (8 hunks)
✅ Files skipped from review due to trivial changes (1)
  • package.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/hooks/courses/useCoursePayment.js
  • src/components/forms/course/embedded/EmbeddedDocumentForm.js
  • src/components/content/courses/tabs/CourseOverview.js
  • src/hooks/courses/useCourseData.js
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/hooks/courses/useCourseTabs.js (2)
src/hooks/useWindowWidth.js (1)
  • useWindowWidth (3-24)
src/hooks/courses/useCourseTabsState.js (3)
  • useCourseTabsState (13-138)
  • tabMap (30-41)
  • toggleTab (60-71)
src/hooks/courses/useCourseTabsState.js (2)
src/hooks/courses/useCourseTabs.js (3)
  • useCourseTabsState (20-33)
  • isMobileView (17-17)
  • toggleTab (54-76)
src/hooks/courses/useCourseNavigation.js (1)
  • toggleTab (62-70)
🔇 Additional comments (24)
src/hooks/courses/index.js (1)

1-17: Good centralized module export pattern.

This index file neatly consolidates and re-exports all course-related hooks, creating a clean entry point that simplifies imports throughout the application.

src/hooks/courses/useCourseNavigation.js (3)

10-45: Well-structured hook with good organization of navigation logic.

This custom hook effectively extracts course navigation logic into a reusable unit, delegating core tab state management to the base useCourseTabsState hook. The initialization based on router parameters and mobile view detection is well-implemented with proper dependency arrays.


61-71: Well-implemented tab toggling with responsive behavior.

The toggleTab function correctly handles mobile vs. desktop behavior for sidebar visibility and is properly memoized with the right dependencies.


72-86: Clean and complete hook exports.

The hook returns a comprehensive set of state variables and functions that cover all necessary navigation functionality.

src/hooks/courses/useCourseTabs.js (4)

6-14: Good use of deprecation notice.

Clearly marking this hook as deprecated and suggesting alternatives helps guide developers to the preferred implementation. This is good documentation practice.


15-34: Effective delegation to base hook.

The hook now properly delegates core functionality to useCourseTabsState, addressing previous redundancy issues. The guard against undefined window width on line 17 is a good defensive coding practice.


35-51: Improved router integration.

The effect now handles both active and tab query parameters, providing better URL synchronization with application state.


52-76: Proper URL synchronization.

The toggleTab implementation now correctly updates the URL query parameters with shallow routing, which was a missing feature identified in a previous review.

src/hooks/encryption/useCourseDecryption.js (5)

4-24: Well-structured hook setup with proper state tracking.

The hook initializes all necessary state and refs, including proper tracking of decrypted lessons, loading state, and processing flags. The addition of timeout refs (lines 11-12) addresses a previous issue with memory leaks.


35-43: Good lesson change detection and retry count reset.

Tracking the last lesson ID and resetting retry counts when lessons change helps prevent excessive retry attempts on difficult-to-decrypt lessons.


44-132: Robust decryption implementation with timeouts and retries.

The decryption function includes:

  • Proper guards to prevent unnecessary processing
  • Retry count tracking with a maximum limit
  • Timeout handling for long-running decryptions
  • Proper cleanup of timeouts
  • Silent error handling to prevent UI disruption

The implementation now properly cleans up timeouts with decryptTimeoutRef.current = null (line 94), addressing a previous issue.


133-142: Appropriate trigger for decryption.

The effect correctly triggers decryption when:

  1. The active lesson changes
  2. The user has access
  3. The lesson isn't already decrypted

143-155: Proper cleanup on unmount.

The hook now includes a cleanup effect that properly clears all timeouts when the component unmounts, addressing a previous issue with potential memory leaks.

src/hooks/courses/useCourseTabsState.js (1)

1-138: Well-designed and thoroughly documented hook for tab state management

This custom hook provides an elegant solution for managing course tab navigation state. I appreciate the comprehensive JSDoc comments, clear structure with separated concerns, and modular approach.

Key strengths:

  • Clear separation between pure state management and side effects
  • Well-implemented keyboard navigation for accessibility
  • Responsive design with mobile/desktop view adaptations
  • Smart initialization and auto-updates for sidebar visibility

The code follows best practices by using appropriate React hooks (useState, useMemo, useCallback, useEffect) for their intended purposes and maintaining clean dependencies arrays.

src/pages/course/[slug]/index.js (10)

1-9: Buffer polyfill properly added – fixes the previous issue

The code now includes the Buffer polyfill from the buffer package, addressing the browser compatibility issue that was correctly identified in previous reviews.


11-27: Good organization of imports with clear grouping and comments

The imports are now well-organized into logical sections with clear comments:

  • React core and hooks
  • Custom hooks
  • Components
  • Configuration

This makes the file more maintainable and improves readability.


38-54: Improved state management through custom course navigation hook

The refactoring has successfully extracted the navigation logic into a custom hook useCourseNavigation. This reduces the complexity of the component and centralizes navigation state management.


71-87: Improved localStorage persistence with error handling

The code now properly handles the loading of completed lessons from localStorage with appropriate error handling. The addition of the course ID and user public key to the storage key improves data isolation.


89-107: Well-implemented completion tracking with duplication prevention

The setCompleted callback now includes deduplication logic and proper localStorage persistence. The approach correctly avoids unnecessary state updates when a lesson is already marked as completed.


133-141: Now passing activeIndex to useCourseDecryption hook

Good update to the useCourseDecryption hook call to include the activeIndex parameter, which helps the hook know which lesson is currently active.


226-234: Clean menu tab implementation with proper handlers

The menu tab implementation now uses the navigation hook's methods properly. The component receives all necessary props from the hook, including tab items, active index, and toggle functions.


236-284: Good separation of concerns with modular tab components

The implementation has been refactored to use dedicated components for each tab type:

  • CourseOverview for the overview tab
  • CourseContent for the content tab
  • CourseQA for the QA tab

This approach greatly improves code organization and maintainability by moving the tab-specific rendering logic to specialized components. I also appreciate the improved loading state handling with the decryption spinner.


305-312: Consistent sidebar implementation with navigation hook

The sidebar implementation now uses the navigation hook's handleLessonSelect method for selecting lessons, maintaining consistency across the UI.


323-329: Clear mobile sidebar handling with proper tab toggling

The mobile sidebar implementation also correctly uses the navigation hook's methods, including handleLessonSelect for selecting lessons and toggleTab with getActiveTabIndex for returning to the previous tab after closing the sidebar.

@AustinKelsay AustinKelsay merged commit e20bf4e into main May 12, 2025
4 checks passed
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.

Refactor/course-and-lessons-page

1 participant