Skip to content

Refactor/course and lesson pages#64

Merged
AustinKelsay merged 30 commits intomainfrom
refactor/course-and-lesson-pages
Apr 30, 2025
Merged

Refactor/course and lesson pages#64
AustinKelsay merged 30 commits intomainfrom
refactor/course-and-lesson-pages

Conversation

@AustinKelsay
Copy link
Copy Markdown
Owner

@AustinKelsay AustinKelsay commented Apr 14, 2025

Summary by CodeRabbit

  • New Features

    • Added a new MarkdownDisplay component for consistent and enhanced markdown rendering with improved styling and support for GitHub Flavored Markdown.
    • Introduced CourseSidebar component for responsive lesson navigation on desktop and mobile.
    • Added CourseHeader component to provide improved course navigation context.
    • Introduced DesktopCourseDetails and MobileCourseDetails components for detailed and responsive course information display.
    • Redesigned course page with a tabbed interface, improved sidebar behavior, and keyboard navigation for better user experience.
  • Refactor

    • Replaced all dynamic markdown preview imports with a static, unified markdown display component across multiple content pages.
    • Simplified course details rendering by delegating to new responsive components.
    • Updated component props and layout to enhance mobile responsiveness and styling consistency.
    • Refactored navigation logic to support shallow routing and conditional rendering based on device type.
  • Chores

    • Added github-markdown-css and remark-breaks dependencies to enhance markdown styling and line break handling.
    • Removed @uiw/react-markdown-preview dependency and its associated CSS import.
    • Cleaned up unused imports and hooks across various components to streamline the codebase.
    • Updated next dependency to the latest patch version for improved stability and performance.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 14, 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 Apr 28, 2025 10:22pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2025

Walkthrough

The pull request updates the markdown rendering logic by replacing dynamic imports of the MDDisplay component with a statically imported MarkdownDisplay across multiple files, along with updating its prop from source to content. Two new dependencies are added in package.json. New components such as CourseHeader, CourseSidebar, and MarkdownDisplay are introduced. Additionally, several course-related pages are refactored to include updated state management, tabbed layouts, and conditional mobile rendering. Minor modifications were also made in MenuTab, Navbar, configuration, and other components.

Changes

File(s) Change Summary
package.json Added dependencies: "github-markdown-css": "^5.8.1" and "remark-breaks": "^4.0.0"; updated next version to ^14.2.28; removed @uiw/react-markdown-preview.
src/components/content/combined/CombinedDetails.js
src/components/content/courses/(CombinedLesson.js, CourseLesson.js, DocumentLesson.js, DraftCourseDetails.js, DraftCourseLesson.js, VideoLesson.js)
src/components/content/(documents/DocumentDetails.js, videos/VideoDetails.js)
src/pages/draft/[slug]/index.js
Replaced dynamic import of MDDisplay with a static import of MarkdownDisplay and updated the prop from source to content.
src/components/markdown/MarkdownDisplay.js Added new MarkdownDisplay component using ReactMarkdown with plugins (remarkGfm, remarkBreaks, rehypeRaw) and custom rendering for links and images.
src/components/content/courses/CourseDetails.js Refactored to delegate rendering to new DesktopCourseDetails and MobileCourseDetails components based on mobile view; added new props isMobileView and showCompletedTag; simplified rendering and styling.
src/components/content/courses/DesktopCourseDetails.js New component rendering detailed course info for desktop, including image, title, tags, author info, payment details, and completion status.
src/components/content/courses/MobileCourseDetails.js New component rendering detailed course info optimized for mobile, with topics, author, zap display, and payment info.
src/components/content/courses/CourseHeader.js New component providing a responsive course header with back navigation, conditional rendering for navbar mode, and routing logic.
src/components/content/courses/CourseSidebar.js New component rendering a sidebar with course lessons, supporting toggling visibility and responsive layout for mobile and desktop.
src/components/menutab/MenuTab.js Extended to accept sidebarVisible, onToggleSidebar, and isMobileView props; added a sidebar toggle button rendered conditionally on desktop.
src/components/navbar/Navbar.js Added logic to detect course pages, fetch course data using useNDKContext and nip19, and render CourseHeader with course details and completion status.
src/pages/course/[slug]/index.js Refactored to implement a tabbed interface for course content using MenuTab; added state variables (activeTab, sidebarVisible, activeIndex); introduced helper functions for tab and sidebar management; replaced accordion navigation with tabs and sidebar; updated markdown rendering to use MarkdownDisplay.
src/pages/draft/[slug]/index.js Replaced dynamic import of MDDisplay with static import of MarkdownDisplay and updated usage accordingly.
src/pages/_app.js Removed import of @uiw/react-markdown-preview CSS file.
Various components (HeroBanner.js, ActivityContributionChart.js, CombinedContributionChart.js, CoursesCarousel.js, DocumentsCarousel.js, MessageDropdownItem.js, ContentListItem.js, MessageInput.js, StackerNewsFeed.js, EmbeddedDocumentForm.js, CommunityMenuTab.js, UserAccountLinking.js, UserProfileCard.js, CalendlyEmbed.js, ZapDisplay.js, ZapForm.js, NDKContext.js, UserProgressTable.js) Removed unused imports such as useEffect, useMemo, Tooltip, axios, nip19, and others to clean up code.
src/config/appConfig.js Removed trailing comma from authorPubkeys array.

Sequence Diagram(s)

sequenceDiagram
    participant U as User
    participant R as Router
    participant CP as Course Page (index.js)
    participant MT as MenuTab
    participant MS as MarkdownDisplay
    participant CS as CourseSidebar

    U->>R: Navigate to course page
    R->>CP: Load course page component
    CP->>CP: Initialize state (activeIndex, activeTab, sidebarVisible)
    CP->>MT: Render MenuTab with sidebar toggle props
    MT->>CP: Toggle sidebar on user interaction
    CP->>CS: Conditionally render CourseSidebar (mobile view)
    CP->>MS: Render course content via MarkdownDisplay
    U-->>CP: Interact with tabs and content
Loading
sequenceDiagram
    participant U as User
    participant N as Navbar
    participant ND as NDK Context
    participant CH as CourseHeader

    U->>N: Access course-related route
    N->>ND: Fetch course data using context and decode slug
    ND-->>N: Return course event data
    N->>CH: Render CourseHeader with course details
    CH->>U: Provide back navigation and responsive header UI
Loading

Possibly related PRs

  • Bg blur2 and zap threads parent #56: The main PR and the retrieved PR both modify components that render content details (e.g., CombinedDetails, DocumentDetails, VideoDetails, Course page) by adding or updating the integration of the ZapThreadsWrapper component with user authorization and key encoding logic, indicating a direct and strong connection at the code level related to threaded comments and access control.
  • Feature - MoreOptions on content details components #53: The main PR replaces dynamic imports of the @uiw/react-markdown-preview component with a static import of a local MarkdownDisplay component across multiple content-related files, focusing on markdown rendering changes, while the retrieved PR adds new menu and toast notification functionality for marking lessons as completed and managing content options in the same content detail components; thus, both PRs modify the same components but address different concerns—markdown rendering vs. user interaction menus—making their changes related but orthogonal in purpose.

Poem

I'm a little rabbit, hopping by with glee,
Updating components so smooth, as neat as can be.
From dynamic to static, the Markdown now gleams,
With new tabs and sidebars fulfilling our dreams.
Hop along the code trail, where logic takes flight 🐇✨.
CodeRabbit Inc. cheers on each new byte!


📜 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 fac14d5 and 14d63d7.

📒 Files selected for processing (1)
  • src/pages/course/[slug]/index.js (8 hunks)
🔇 Additional comments (7)
src/pages/course/[slug]/index.js (7)

95-124: Great optimization in lesson fetching!

The code now batches all lesson requests into a single API call with a combined filter, which is more efficient than making multiple individual API requests. This should improve performance, especially for courses with many lessons.


194-202: Good use of memoization for the tab map.

Using useMemo for the tab map prevents unnecessary recalculations when the component re-renders. The code correctly depends only on isMobileView in the dependency array.


204-217: Good implementation of error handling for missing naddress.

This properly addresses the previous TODO comment by implementing error handling when no valid naddress is found in the URL slug. The toast notification and redirect to the courses page after a delay provide good UX.


405-429: Well-structured QA section with proper authorization checks.

The QA section is well-implemented with appropriate authorization checks to ensure only authorized users (purchasers, subscribers, or content creators) can access the comments section.


536-567: Clean implementation of tabbed content with conditional rendering.

The tabbed content area is well-organized with conditionally rendered sections based on the active tab. This provides a cleaner user experience and better organization of content.


570-627: Good responsive design with different sidebar implementations for desktop and mobile.

The sidebar implementation is well thought out with different behavior for desktop and mobile views:

  • On desktop, it's positioned absolutely with smooth transitions
  • On mobile, it's shown conditionally based on the active tab

This approach enhances the user experience across different device sizes.


613-615: Good tab selection approach in onLessonSelect.

Using setActiveTab('content') directly rather than finding the tab by label is more robust and less error-prone.

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

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

32-35: Improve keyboard accessibility for the clickable div

The div with onClick handler should be accessible to keyboard users. Consider using a button element or adding keyboard event handlers.

<div 
-  className="flex items-center cursor-pointer hover:opacity-90"
+  className="flex items-center cursor-pointer hover:opacity-90"
+  role="button"
+  tabIndex={0}
   onClick={() => router.push(`/course/${router.query.slug}`)}
+  onKeyDown={(e) => {
+    if (e.key === 'Enter' || e.key === ' ') {
+      router.push(`/course/${router.query.slug}`);
+    }
+  }}
>
src/components/content/courses/CourseSidebar.js (1)

141-142: Consider making the height calculation more flexible.

The fixed height calculation (h-[calc(100vh-400px)]) might be brittle if other layout elements change size. Consider using a more dynamic approach based on the actual size of other elements, or at least extract this calculation to a variable with a clear comment explaining the 400px offset.

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

25-26: Inline state for course and completion status.
Storing course and isCompleted in local state is straightforward. However, if the codebase grows, consider encapsulating completion logic within a helper to reduce repeated condition checks throughout.


28-29: Potential route matching edge cases.
router.pathname.startsWith('/course/') works if no subpath ambiguity exists (e.g., /course-like/). If unexpected collisions might occur, consider a more robust route check or Next.js route param usage.


31-68: Side effect fetch logic.
Fetching course data in useEffect based on isCoursePage and session info is sensible. A few considerations:

  • The effect re-runs on every relevant dependency change (e.g., session?.user?.completedCourses). Ensure this won't cause redundant network calls if the session updates frequently.
  • Error handling currently logs to console; consider a user-facing fallback (e.g., a toast or a minimal UI feedback) if the fetch fails.
    Otherwise, the flow looks solid.

117-142: Conditional branding vs. course header.
Conditionally displaying CourseHeader is correct. If the course is still loading, though, users might see the standard branding briefly. Consider a loading state in the navbar or a placeholder if that flicker is undesirable.

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

45-46: Mobile detection fallback.
localIsMobileView is a nice fallback if the parent doesn’t provide isMobileView. Just be mindful of duplicating logic (e.g., if there's a single source of truth in the app for mobile detection).

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

183-192: Auto-setting nAddress from slug.
Fetching nAddress if the slug includes 'naddr' is consistent with your NIP-19 usage. Consider also handling slug validations (empty or incorrectly formatted) with user feedback.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 24e8a51 and 03789e8.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (20)
  • package.json (2 hunks)
  • src/components/content/combined/CombinedDetails.js (3 hunks)
  • src/components/content/courses/CombinedLesson.js (3 hunks)
  • src/components/content/courses/CourseDetails.js (4 hunks)
  • src/components/content/courses/CourseHeader.js (1 hunks)
  • src/components/content/courses/CourseLesson.js (3 hunks)
  • src/components/content/courses/CourseSidebar.js (1 hunks)
  • src/components/content/courses/DocumentLesson.js (3 hunks)
  • src/components/content/courses/DraftCourseDetails.js (2 hunks)
  • src/components/content/courses/DraftCourseLesson.js (2 hunks)
  • src/components/content/courses/VideoLesson.js (3 hunks)
  • src/components/content/documents/DocumentDetails.js (3 hunks)
  • src/components/content/videos/VideoDetails.js (3 hunks)
  • src/components/markdown/MarkdownDisplay.js (1 hunks)
  • src/components/menutab/MenuTab.js (2 hunks)
  • src/components/navbar/Navbar.js (3 hunks)
  • src/components/profile/DataTables/UserProgressTable.js (0 hunks)
  • src/config/appConfig.js (1 hunks)
  • src/pages/course/[slug]/index.js (7 hunks)
  • src/pages/draft/[slug]/index.js (2 hunks)
💤 Files with no reviewable changes (1)
  • src/components/profile/DataTables/UserProgressTable.js
🧰 Additional context used
🧬 Code Graph Analysis (14)
src/components/content/courses/DraftCourseLesson.js (1)
src/components/markdown/MarkdownDisplay.js (1)
  • MarkdownDisplay (8-25)
src/components/content/documents/DocumentDetails.js (2)
src/components/markdown/MarkdownDisplay.js (1)
  • MarkdownDisplay (8-25)
src/pages/details/[slug]/index.js (1)
  • decryptedContent (22-22)
src/components/menutab/MenuTab.js (2)
src/pages/course/[slug]/index.js (2)
  • isMobileView (179-179)
  • sidebarVisible (176-176)
src/components/buttons/GenericButton.js (1)
  • GenericButton (4-35)
src/components/content/combined/CombinedDetails.js (2)
src/components/markdown/MarkdownDisplay.js (1)
  • MarkdownDisplay (8-25)
src/pages/details/[slug]/index.js (1)
  • decryptedContent (22-22)
src/components/content/videos/VideoDetails.js (2)
src/components/markdown/MarkdownDisplay.js (1)
  • MarkdownDisplay (8-25)
src/pages/details/[slug]/index.js (1)
  • decryptedContent (22-22)
src/components/content/courses/DocumentLesson.js (1)
src/components/markdown/MarkdownDisplay.js (1)
  • MarkdownDisplay (8-25)
src/components/content/courses/DraftCourseDetails.js (1)
src/components/markdown/MarkdownDisplay.js (1)
  • MarkdownDisplay (8-25)
src/components/content/courses/VideoLesson.js (1)
src/components/markdown/MarkdownDisplay.js (1)
  • MarkdownDisplay (8-25)
src/components/content/courses/CourseDetails.js (3)
src/hooks/useImageProxy.js (1)
  • returnImageProxy (6-26)
src/components/zaps/ZapDisplay.js (1)
  • ZapDisplay (7-46)
src/components/ui/MoreOptionsMenu.js (1)
  • MoreOptionsMenu (14-67)
src/components/content/courses/CourseSidebar.js (5)
src/components/content/courses/CourseDetails.js (1)
  • useImageProxy (40-40)
src/components/navbar/Navbar.js (1)
  • navbarHeight (19-19)
src/pages/course/[slug]/index.js (5)
  • navbarHeight (181-181)
  • activeIndex (174-174)
  • isMobileView (179-179)
  • completedLessons (175-175)
  • lessons (91-91)
src/hooks/useImageProxy.js (1)
  • returnImageProxy (6-26)
src/components/buttons/GenericButton.js (1)
  • GenericButton (4-35)
src/components/content/courses/CombinedLesson.js (1)
src/components/markdown/MarkdownDisplay.js (1)
  • MarkdownDisplay (8-25)
src/components/content/courses/CourseLesson.js (1)
src/components/markdown/MarkdownDisplay.js (1)
  • MarkdownDisplay (8-25)
src/pages/course/[slug]/index.js (3)
src/components/content/courses/CourseSidebar.js (2)
  • navbarHeight (20-20)
  • CourseSidebar (7-152)
src/components/ZapThreadsWrapper.js (1)
  • ZapThreadsWrapper (3-56)
src/components/menutab/MenuTab.js (1)
  • MenuTab (6-45)
src/components/navbar/Navbar.js (2)
src/components/content/courses/CourseHeader.js (2)
  • router (12-12)
  • CourseHeader (6-80)
src/utils/nostr.js (2)
  • parseCourseEvent (149-218)
  • parseCourseEvent (149-218)
🪛 Biome (1.9.4)
src/components/content/courses/CourseDetails.js

[error] 341-344: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

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

15-15: LGTM: New author public key added.

You've added a new public key to the authorPubkeys array, which will allow an additional author to publish content in the application.

package.json (1)

41-41: Good job adding dependencies to support the Markdown rendering.

These dependencies align well with your refactoring goals:

  • github-markdown-css will provide consistent styling for the markdown content
  • remark-breaks will handle line breaks properly in the markdown

Both of these support the new MarkdownDisplay component that's replacing the dynamic MDDisplay imports.

Also applies to: 54-54

src/components/content/courses/DraftCourseLesson.js (1)

9-9: Successfully refactored to use the new MarkdownDisplay component.

You've replaced the dynamic import of MDDisplay with a static import of MarkdownDisplay and updated the prop from source to content. This change is consistent with the overall refactoring approach in this PR and brings several benefits:

  • Improved performance by using static imports
  • Better markdown rendering with GitHub styling
  • Proper handling of line breaks through the remarkBreaks plugin
  • Custom rendering for links and images

Also applies to: 148-148

src/components/content/combined/CombinedDetails.js (1)

17-17: Successfully refactored both Markdown rendering instances.

You've consistently implemented the MarkdownDisplay component in both scenarios:

  1. For rendering decrypted content (paid content)
  2. For rendering regular content

The prop change from source to content is appropriate, and you've maintained different styling for each use case with appropriate class names. This change aligns well with the overall refactoring approach in the PR.

Also applies to: 204-204, 232-232

src/components/content/documents/DocumentDetails.js (3)

17-17: Appropriate import replacement for Markdown rendering.

The static import of MarkdownDisplay replaces the previous dynamic import of MDDisplay. This is part of a broader refactoring to standardize Markdown rendering across the application.


210-210: Prop name correctly updated to match new component requirements.

The source prop has been replaced with content to match the API of the new MarkdownDisplay component. This aligns with the component's expected props as defined in src/components/markdown/MarkdownDisplay.js.


236-236: Consistent prop naming in second usage.

This second instance of MarkdownDisplay also correctly uses the content prop instead of source, maintaining consistency throughout the application.

src/components/content/courses/DraftCourseDetails.js (2)

20-20: Static import replaces dynamic import for Markdown rendering.

The import of MarkdownDisplay aligns with the application-wide refactoring of Markdown display components.


466-467: Prop name updated to follow new component requirements.

The content prop is now correctly used instead of source when passing the Markdown content to the MarkdownDisplay component.

src/components/content/courses/DocumentLesson.js (3)

16-16: Appropriate import for standardized Markdown rendering.

The static import of MarkdownDisplay aligns with the application-wide refactoring of Markdown display components.


117-117: Prop name updated for paid decrypted content rendering.

The content prop is correctly used for the first use case - rendering paid content that has been decrypted.


132-132: Prop name updated for regular content rendering.

The second instance also correctly uses the content prop for rendering the lesson content when available.

src/components/menutab/MenuTab.js (4)

3-4: Added necessary imports for sidebar toggle functionality.

The imports for Button and GenericButton are needed for the new sidebar toggle feature being added to this component.


6-6: Component signature updated to support sidebar functionality.

The function signature has been extended to accept three new parameters: sidebarVisible, onToggleSidebar, and isMobileView with a sensible default. This aligns with the updated requirements for the component as seen in the course pages.


8-8: Added positioning context for toggle button.

The relative class added to the wrapper div is necessary for the absolute positioning of the sidebar toggle button.


25-42: Added conditional sidebar toggle button with appropriate behavior.

The sidebar toggle button implementation:

  1. Is conditionally rendered only in non-mobile views
  2. Changes its icon based on the sidebar visibility state
  3. Includes accessibility features (tooltip and aria-label)
  4. Is properly positioned at the right side of the component

This enhances the user experience by providing an intuitive way to toggle the course lessons sidebar.

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

1-27: Good implementation of the Markdown renderer component!

The component is well-structured and follows best practices: checks for empty content, applies proper styling with GitHub markdown theme, and implements security measures for external links. The customization of link and image components is a nice touch.

src/components/content/courses/CourseHeader.js (1)

57-80: LGTM: Well-structured course header with good navigation logic

The implementation of the standard mode header is clean, with good use of positioning, responsive design, and navigation handling.

src/components/content/courses/CombinedLesson.js (3)

17-17: LGTM: Good replacement of dynamic import with static import

This change simplifies the component usage and potentially improves performance by using static imports.


174-174: Successfully updated component and prop name

The component has been properly updated from MDDisplay to MarkdownDisplay, with the prop name changed from source to content.


216-216: Successfully updated component and prop name

The component has been properly updated from MDDisplay to MarkdownDisplay, with the prop name changed from source to content.

src/components/content/courses/CourseLesson.js (3)

15-15: LGTM: Good replacement of dynamic import with static import

This change simplifies the component usage and potentially improves performance by using static imports.


113-113: Successfully updated component and prop name

The component has been properly updated from MDDisplay to MarkdownDisplay, with the prop name changed from source to content.


123-123: Successfully updated component and prop name

The component has been properly updated from MDDisplay to MarkdownDisplay, with the prop name changed from source to content.

src/components/content/videos/VideoDetails.js (3)

17-17: Clean import replacement with MarkdownDisplay component.

The change from dynamic import of MDDisplay to a static import of MarkdownDisplay is a good improvement that should simplify the code and potentially improve load performance.


207-207: Properly updated component usage with correct props.

The component has been correctly updated to use the content prop instead of source while maintaining the same className for consistent styling.


240-240: Properly updated component usage with correct props.

Same as above, the component has been correctly updated to use the content prop while maintaining styling consistency.

src/components/content/courses/VideoLesson.js (3)

16-16: Clean import replacement with MarkdownDisplay component.

The change from dynamic import of MDDisplay to a static import of MarkdownDisplay is a good improvement that should simplify the code and potentially improve load performance.


169-169: Properly updated component usage with correct props.

The component has been correctly updated to use the content prop instead of source while maintaining the same className for consistent styling and preserving the ref usage for tracking video elements.


195-195: Properly updated component usage with correct props.

Same as above, the component has been correctly updated to use the content prop while maintaining styling consistency and preserving the ref for tracking functionality.

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

22-22: Clean import replacement with MarkdownDisplay component.

The change from dynamic import of MDDisplay to a static import of MarkdownDisplay is a good improvement that should simplify the code and potentially improve load performance.


432-432: Properly updated component usage with correct props.

The component has been correctly updated to use the content prop instead of source while maintaining the conditional rendering check for content availability.

src/components/content/courses/CourseSidebar.js (2)

1-155: Well-structured new component for course navigation.

This new CourseSidebar component is well-designed with good separation of concerns, handling both mobile and desktop layouts appropriately. The state management with parent component synchronization is implemented effectively.


50-80: Well-implemented lesson item with responsive design.

The LessonItem component efficiently handles both mobile and desktop views with appropriate conditional styling. The use of Image component with proper alt text and the completed tag display is well implemented.

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

11-14: New imports are appropriate.
These imports are utilized correctly in the subsequent code (e.g., useNDKContext, nip19.decode, parseCourseEvent, CourseHeader). No issues identified.


24-24: Use of NDK context.
Acquiring ndk from the context is a well-structured approach; it cleanly centralizes networking/fetch capabilities.

src/components/content/courses/CourseDetails.js (2)

24-24: New Divider import.
This import is used effectively later for clarity in the UI layout.


33-34: Expanded component props.
Allowing isMobileView and showCompletedTag to be passed in increases this component’s versatility. Good addition to support multiple device layouts and toggling completion labels.

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

1-21: Additional imports and dependencies.
Pulling in useMemo, useWindowWidth, MenuTab, MarkdownDisplay, and others indicates a well-structured approach to unify your new tabbed UI and markdown rendering. No issues here.


174-181: State for lesson navigation and layout.
activeIndex, sidebarVisible, nAddress, isMobileView, and activeTab are nicely separated. This should keep the code organized for multi-tab logic and responsive design.


194-210: Automated tab selection for lesson routes.
Switching to 'content' tab when active is specified is a great user experience. The logic for toggling sidebarVisible based on mobile vs. desktop is also clear.


248-254: Memoizing course completion.
Using useMemo to check if any lesson is completed is an efficient approach for derived state. If the completion logic grows (e.g., all lessons must be completed), you can adapt the check easily.


256-265: handleLessonSelect sets route and tab.
This ensures direct linking to a lesson while seamlessly switching tabs on mobile. Nicely done.


284-302: Tab switching logic.
Mapping tab indices to labels is a clean approach. Special-casing 'lessons' for mobile is well-handled.


304-306: Sidebar toggle.
Straightforward state toggle for desktop usage.


308-316: getActiveTabIndex for bridging name→index.
Keeps MenuTab in sync with your custom tab logic. Good design.


318-346: Dynamically building tab items.
Injecting the “Lessons” tab only on mobile is a nice example of adaptive UI.


348-371: renderQASection with ZapThreadsWrapper.
This is a good modular approach for embedding Nostr-based comments. The fallback text Loading comments... is helpful to the user.


373-412: renderOverviewSection with .
Leverages the newly introduced isMobileView prop to show a tailored layout. The approach is consistent with the rest of the code.


459-575: Tab-based course layout and sidebar.
The entire restructured layout is coherent. Toggling the sidebar for desktop vs. mobile is a nice user experience touch. The final code is somewhat large but well segmented in logical blocks (overview, content, lessons, QA), making it fairly maintainable.

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

🧹 Nitpick comments (13)
src/components/content/courses/CourseSidebar.js (4)

1-21: Extract shared navbar height to maintain consistency
If navbarHeight is frequently used across multiple components, consider centralizing it in a shared config or constants file to avoid discrepancies.


37-82: Refine alt text for lesson images
Using "Lesson ${index + 1} thumbnail" is acceptable, but consider a more descriptive alt text, for instance including lesson.title, to improve accessibility.


110-124: Avoid repetition with shared components
“Course Lessons” heading and lesson listing logic is also present in SidebarContent. Extracting them into a reusable subcomponent could help maintain DRY principles.


126-151: Verify sticky offset
You are using a fixed numerical offset (navbarHeight + 70). Ensure it stays aligned with any global layout changes to prevent overlap or visual misalignment.

src/components/content/courses/DesktopCourseDetails.js (1)

94-112: Optional chain recommendation
To simplify description rendering, consider using an optional chain instead of &&, as static analysis suggests.

Apply this diff at lines 102-105:

- {processedEvent.description &&
-   processedEvent.description
-     .split('\n')
-     .map((line, index) => <p key={index} className="mb-2">{line}</p>)}
+ {processedEvent.description?.split('\n')
+   .map((line, index) => <p key={index} className="mb-2">{line}</p>)}
🧰 Tools
🪛 Biome (1.9.4)

[error] 102-105: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/components/content/courses/MobileCourseDetails.js (3)

51-57: Improve image accessibility.

The alt text "avatar image" is not descriptive enough. For better accessibility, describe whose avatar it is.

<Image
- alt="avatar image"
+ alt={`Avatar of ${author?.username || author?.name || 'course creator'}`}
  src={returnImageProxy(author?.avatar, author?.pubkey)}
  width={32}
  height={32}
  className="rounded-full mr-2"
/>

85-88: Use optional chaining for description rendering.

As suggested by the static analysis, use optional chaining to make the code more robust against undefined values.

- {processedEvent.description &&
-   processedEvent.description
-     .split('\n')
-     .map((line, index) => <p key={index} className="text-sm mb-2">{line}</p>)}
+ {processedEvent.description?.split('\n')
+   .map((line, index) => <p key={index} className="text-sm mb-2">{line}</p>)}
🧰 Tools
🪛 Biome (1.9.4)

[error] 85-88: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


9-22: Consider adding prop validation.

This component receives many props, but lacks prop validation. Consider adding PropTypes or TypeScript typing to ensure the correct props are passed.

You could add prop validation using PropTypes:

import PropTypes from 'prop-types';

// After the component definition
MobileCourseDetails.propTypes = {
  processedEvent: PropTypes.object.isRequired,
  paidCourse: PropTypes.bool,
  lessons: PropTypes.array.isRequired,
  decryptionPerformed: PropTypes.bool,
  author: PropTypes.object,
  zapAmount: PropTypes.number,
  zapsLoading: PropTypes.bool,
  menuItems: PropTypes.array,
  returnImageProxy: PropTypes.func.isRequired,
  renderPaymentMessage: PropTypes.func.isRequired,
  isCompleted: PropTypes.bool,
  showCompletedTag: PropTypes.bool
};
src/pages/course/[slug]/index.js (5)

319-345: Memoize tab items to prevent unnecessary recalculations.

The getTabItems function creates a new array on each render. Consider using useMemo to optimize performance, especially since this is used in the render cycle.

- // Create tab items for MenuTab
- const getTabItems = () => {
+ // Memoize tab items
+ const tabItems = useMemo(() => {
    const items = [
      {
        label: 'Overview',
        icon: 'pi pi-home',
      },
      {
        label: 'Content',
        icon: 'pi pi-book',
      }
    ];
    
    // Add lessons tab only on mobile
    if (isMobileView) {
      items.push({
        label: 'Lessons',
        icon: 'pi pi-list',
      });
    }
    
    items.push({
      label: 'Comments',
      icon: 'pi pi-comments',
    });
    
    return items;
- };
+ }, [isMobileView]);

Then update all references from getTabItems() to just tabItems.


347-371: Memoize the QA section to prevent unnecessary re-renders.

The renderQASection function creates a new component tree on each render. Consider memoizing it for better performance since it depends on only a few props.

- // Render the QA section (empty for now)
- const renderQASection = () => {
+ // Memoize the QA section
+ const qaSection = useMemo(() => {
    return (
      <div className="rounded-lg p-8 mt-4 bg-gray-800">
        <h2 className="text-xl font-bold mb-4">Comments</h2>
        {nAddress !== null ? (
          <ZapThreadsWrapper
            anchor={nAddress}
            user={session?.user?.pubkey ? nip19.npubEncode(session?.user?.pubkey) : null}
            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/"
            disable="zaps"
          />
        ) : course?.d ? (
          <ZapThreadsWrapper
            anchor={course.d}
            user={session?.user?.pubkey ? nip19.npubEncode(session?.user?.pubkey) : null}
            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/"
            disable="zaps"
          />
        ) : (
          <p>Loading comments...</p>
        )}
      </div>
    );
- };
+ }, [nAddress, session?.user?.pubkey, course?.d]);

Then replace renderQASection() calls with qaSection.


479-480: Remove unnecessary styles and improve clarity.

The conditional styling approach is unnecessarily complex. The inline style is applied only when both conditions are true, but the className has redundant width values.

- <div className={`transition-all duration-500 ease-in-out ${isMobileView ? 'w-full' : 'w-full'}`} 
-     style={!isMobileView && sidebarVisible ? {paddingRight: '320px'} : {}}>
+ <div className="transition-all duration-500 ease-in-out w-full" 
+     style={!isMobileView && sidebarVisible ? {paddingRight: '320px'} : {}}>

540-543: Simplify conditional class expression.

The conditional class expression is overly complex and could be simplified for better readability.

- <div className={`flex-shrink-0 transition-all duration-300 z-[999] ${
-   (isMobileView && activeTab === 'lessons') ? 'ml-0 w-auto opacity-100' : 
-   'w-0 ml-0 opacity-0 overflow-hidden'
- }`}>
+ <div className={`flex-shrink-0 transition-all duration-300 z-[999] ${
+   (isMobileView && activeTab === 'lessons') ? 'w-auto opacity-100' : 
+   'w-0 opacity-0 overflow-hidden'
+ }`}>

189-190: Implement missing functionality for non-naddr slugs.

There's a TODO comment about handling non-naddr slugs, but no implementation. This could lead to broken functionality if the course is accessed using a regular slug.

if (slug.includes('naddr')) {
  setNAddress(slug);
} else {
- // todo: no naddress?
+ // Create nAddress from course data when available
+ if (course?.pubkey && course?.kind && course?.d) {
+   const naddr = nip19.naddrEncode({
+     pubkey: course.pubkey,
+     kind: course.kind,
+     identifier: course.d,
+     relays: ["wss://relay.nostr.band/", "wss://nos.lol/"]
+   });
+   setNAddress(naddr);
+ }
}

Would you like me to implement this functionality completely or open an issue to track this task?

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 03789e8 and 696effe.

📒 Files selected for processing (5)
  • src/components/content/courses/CourseDetails.js (4 hunks)
  • src/components/content/courses/CourseSidebar.js (1 hunks)
  • src/components/content/courses/DesktopCourseDetails.js (1 hunks)
  • src/components/content/courses/MobileCourseDetails.js (1 hunks)
  • src/pages/course/[slug]/index.js (7 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/components/content/courses/CourseSidebar.js (5)
src/components/content/courses/CourseDetails.js (1)
  • useImageProxy (39-39)
src/pages/course/[slug]/index.js (5)
  • navbarHeight (181-181)
  • activeIndex (174-174)
  • isMobileView (179-179)
  • completedLessons (175-175)
  • lessons (91-91)
src/components/navbar/Navbar.js (1)
  • navbarHeight (19-19)
src/hooks/useImageProxy.js (1)
  • returnImageProxy (6-26)
src/components/buttons/GenericButton.js (1)
  • GenericButton (4-35)
src/components/content/courses/CourseDetails.js (2)
src/components/content/courses/MobileCourseDetails.js (1)
  • MobileCourseDetails (9-134)
src/components/content/courses/DesktopCourseDetails.js (1)
  • DesktopCourseDetails (9-151)
src/components/content/courses/DesktopCourseDetails.js (7)
src/components/content/courses/MobileCourseDetails.js (1)
  • router (23-23)
src/components/content/courses/CourseDetails.js (5)
  • router (38-38)
  • zapAmount (35-35)
  • menuItems (62-80)
  • author (36-36)
  • renderPaymentMessage (126-193)
src/pages/course/[slug]/index.js (3)
  • router (170-170)
  • lessons (91-91)
  • paidCourse (26-26)
src/hooks/useImageProxy.js (1)
  • returnImageProxy (6-26)
src/components/zaps/ZapDisplay.js (1)
  • ZapDisplay (7-46)
src/hooks/nostrQueries/zaps/useZapsSubscription.js (1)
  • zapsLoading (7-7)
src/components/ui/MoreOptionsMenu.js (1)
  • MoreOptionsMenu (14-67)
src/pages/course/[slug]/index.js (5)
src/components/content/courses/CourseDetails.js (6)
  • nAddress (37-37)
  • windowWidth (43-43)
  • router (38-38)
  • fetchAuthor (88-99)
  • session (41-41)
  • CourseDetails (25-233)
src/hooks/useWindowWidth.js (1)
  • useWindowWidth (3-24)
src/components/content/courses/CourseSidebar.js (2)
  • navbarHeight (20-20)
  • CourseSidebar (7-152)
src/components/ZapThreadsWrapper.js (1)
  • ZapThreadsWrapper (3-56)
src/components/menutab/MenuTab.js (1)
  • MenuTab (6-45)
🪛 Biome (1.9.4)
src/components/content/courses/DesktopCourseDetails.js

[error] 102-105: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/components/content/courses/MobileCourseDetails.js

[error] 85-88: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (12)
src/components/content/courses/CourseSidebar.js (2)

22-35: Prop-driven visibility sync looks good
The effect ensures the sidebar’s local visible state is updated whenever parentSidebarVisible changes. Code appears logical and robust.


84-108: Structured sidebar content
The header and close icon handling are straightforward and easy to maintain. Good separation of concerns here.

src/components/content/courses/CourseDetails.js (5)

21-23: New mobile/desktop imports
Introducing both DesktopCourseDetails and MobileCourseDetails keeps the layout code clean and well-organized.


32-33: Enhanced configuration
Adding isMobileView and showCompletedTag broadens flexibility for rendering conditional states. Implementation looks solid.


44-45: Phone detection fallback
Combining isMobileView with a local window-width check ensures comprehensive mobile detection. Nicely done.


203-217: Centralized props for clarity
Collecting shared props in detailsProps reduces repetition and improves readability across desktop and mobile components.


220-229: Conditional rendering flow
Switching between mobile and desktop details with isPhone is a clean approach. No issues spotted in the final return statement.

src/components/content/courses/DesktopCourseDetails.js (5)

1-8: Initial imports and setup
Imports are correct, referencing next/image and other required dependencies without redundancy.


9-22: Well-defined prop signature
All props—course data, rendering callbacks, and flags—are encapsulated neatly. Solid structure for the component’s needs.


25-60: Header layout
Displays course image, completion status, and user options effectively. The structure is appropriately modular and user-friendly.


62-90: Topic tags and author details
Proper usage of tags; the avatar is fetched via returnImageProxy. No issues found with data rendering here.


114-148: Detailed course info
Displays lesson count, pricing, and publication date cleanly. Implementation aligns with best practices for content layout.

@AustinKelsay
Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 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: 0

♻️ Duplicate comments (1)
src/pages/course/[slug]/index.js (1)

367-392: 🛠️ Refactor suggestion

Extract relay URLs to a constant.

The relay URLs are duplicated in the renderQASection function at lines 376 and 383.

+ // Add at the top of the component or in a shared config file
+ const NOSTR_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 renderQASection:
<ZapThreadsWrapper
  anchor={nAddress}
  user={session?.user?.pubkey ? nip19.npubEncode(session?.user?.pubkey) : null}
- 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_RELAYS}
  disable="zaps"
/>

// And in the second instance:
<ZapThreadsWrapper
  anchor={course.d}
  user={session?.user?.pubkey ? nip19.npubEncode(session?.user?.pubkey) : null}
- 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_RELAYS}
  disable="zaps"
/>
🧹 Nitpick comments (2)
src/components/content/courses/MobileCourseDetails.js (2)

30-36: Implement optional chaining for topics display.

The component handles the display of course topics correctly but could benefit from using optional chaining.

-          {processedEvent.topics &&
-            processedEvent.topics.length > 0 &&
-            processedEvent.topics.map((topic, index) => (
+          {processedEvent.topics?.length > 0 &&
+            processedEvent.topics.map((topic, index) => (

83-96: Course description section with proper text handling.

The component displays the course description with proper formatting by splitting text into paragraphs.

Based on the static analysis hint, consider using optional chaining for the description to prevent potential errors:

-            {processedEvent.description &&
-              processedEvent.description
-                .split('\n')
-                .map((line, index) => <p key={index} className="text-sm mb-2">{line}</p>)}
+            {processedEvent.description?.split('\n')
+              .map((line, index) => <p key={index} className="text-sm mb-2">{line}</p>)}
🧰 Tools
🪛 Biome (1.9.4)

[error] 86-89: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 696effe and a6b442c.

📒 Files selected for processing (3)
  • src/components/content/courses/MobileCourseDetails.js (1 hunks)
  • src/config/appConfig.js (1 hunks)
  • src/pages/course/[slug]/index.js (7 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/config/appConfig.js
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/pages/course/[slug]/index.js (3)
src/components/content/courses/CourseSidebar.js (2)
  • navbarHeight (20-20)
  • CourseSidebar (7-152)
src/components/menutab/MenuTab.js (1)
  • MenuTab (6-45)
src/components/markdown/MarkdownDisplay.js (1)
  • MarkdownDisplay (8-25)
src/components/content/courses/MobileCourseDetails.js (7)
src/components/content/courses/DesktopCourseDetails.js (1)
  • router (23-23)
src/pages/course/[slug]/index.js (3)
  • router (170-170)
  • lessons (91-91)
  • paidCourse (26-26)
src/components/content/courses/CourseDetails.js (5)
  • router (38-38)
  • zapAmount (35-35)
  • author (36-36)
  • menuItems (62-80)
  • renderPaymentMessage (126-193)
src/components/zaps/ZapDisplay.js (1)
  • ZapDisplay (7-46)
src/hooks/nostrQueries/zaps/useZapsSubscription.js (1)
  • zapsLoading (7-7)
src/hooks/useImageProxy.js (1)
  • returnImageProxy (6-26)
src/components/ui/MoreOptionsMenu.js (1)
  • MoreOptionsMenu (14-67)
🪛 Biome (1.9.4)
src/components/content/courses/MobileCourseDetails.js

[error] 86-89: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (17)
src/components/content/courses/MobileCourseDetails.js (6)

1-8: Correct imports usage for MobileCourseDetails.

The component imports all necessary dependencies, including React, Next.js components, and PrimeReact UI components.


9-22: Well-structured props interface for MobileCourseDetails.

The component accepts a comprehensive set of props for displaying course details, handling payment actions, and managing user interactions in a mobile context.


23-24: Router initialization for navigation.

The component correctly initializes the Next.js router for handling navigation.


38-46: Effective title and zaps display.

The component correctly displays the course title and zap amount in a responsive layout with proper alignment.


48-75: Author information and more options display.

The author information section is well-implemented with proper avatar display, username fallback, and external link handling.


98-131: Well-structured course information section.

The component displays course information in a well-organized layout, showing lesson count, price (if applicable), and publication date with appropriate conditional rendering.

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

1-21: Updated imports for enhanced course page functionality.

The component imports additional critical dependencies for the new tabbed interface and responsive layout, including useMemo, CourseSidebar, CourseHeader, MenuTab, and MarkdownDisplay.


174-182: Updated state management for course navigation.

The component replaces expandedIndex with activeIndex and adds new state variables for sidebar visibility, nAddress, and tab management, along with responsive view detection.


183-210: Effective initialization logic for course navigation.

The useEffect hooks correctly initialize the nAddress and handle tab/lesson selection based on URL parameters, with proper responsive behavior handling for sidebar visibility.


248-265: Improved course completion logic and lesson selection.

The component uses useMemo for checking course completion status and implements an improved lesson selection handler with router integration.


284-316: Well-structured tab navigation logic.

The component implements comprehensive tab management with proper mapping between tab indices and names, handling mobile-specific logic correctly.


318-345: Dynamic tab items generation based on device type.

The component intelligently generates tab items based on whether the view is mobile or desktop, adjusting the UI appropriately.


347-365: Keyboard navigation support for accessibility.

The implementation adds keyboard navigation support for tabs, enhancing accessibility.


393-432: Well-implemented overview section with responsive design.

The renderOverviewSection function properly handles the course overview display with conditional rendering for mobile view and completed course status.


479-494: Improved navigation with sticky tab menu.

The implementation uses a sticky tab navigation system at the top of the page, positioned correctly relative to the navbar.


495-529: Responsive content layout with tab-based content switching.

The main content area is well-structured with proper transitions between tabs, conditional rendering, and responsive width adjustments.


530-584: Responsive sidebar implementation with device-specific behavior.

The sidebar implementation differs between desktop and mobile views, with appropriate positioning, animations, and interaction handling for each context.

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

♻️ Duplicate comments (2)
src/components/content/courses/VideoLesson.js (1)

195-195: Duplicate comment regarding raw HTML rendering.

This MarkdownDisplay call shares the same XSS concern if lesson.content is untrusted. Review sanitization measures or confirm that the content is safe.

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

367-391: Repeated relay URLs.
These relay URLs are hardcoded here and also appear in other places. This matches a previous recommendation to extract them into a constant or config.

🧹 Nitpick comments (14)
src/components/content/courses/CourseSidebar.js (3)

37-82: Use a stable key for list items.

Currently, each <LessonItem> is keyed by its index. If the lesson list changes order or is updated dynamically, it can lead to stale rendering. If possible, use a unique identifier like lesson.id.

- {lessons.map((lesson, index) => (
-   <LessonItem key={index} ...
+ {lessons.map((lesson) => (
+   <LessonItem key={lesson.id} ...

85-109: Refactor repeated lesson listing into a shared component.

SidebarContent duplicates the lesson iteration logic found in MobileLessonsTab. Extracting a shared <LessonList> or <Lessons> component can reduce code duplication and simplify maintenance.


111-124: MobileLessonsTab duplicates lesson rendering.

As noted above, centralizing the structure for lessons into a shared component would reduce duplication between desktop sidebar and mobile layout.

src/components/content/courses/CourseDetails.js (2)

32-33: Optional chaining for isMobileView

Accepting isMobileView as a prop and defaulting showCompletedTag to true helps maintain flexible layouts. Make sure to document these props in the component’s docstring or usage references.


44-45: Dual mobile detection logic.

You're combining isMobileView with localIsMobileView into isPhone. This is fine for a short-term fix. However, consider consolidating the logic to one approach to avoid potential confusion.

src/components/content/courses/DesktopCourseDetails.js (2)

82-90: Missing href on anchor tag.

This anchor is displayed as a link but does not have an href attribute. If navigation is intended, add an href; otherwise, convert it to a <span> or <div>.

<a
  rel="noreferrer noopener"
  target="_blank"
+ href={`/profile/${author?.pubkey || ''}`}
  className="text-blue-300 hover:underline"
>
  {author?.username || author?.name || author?.pubkey}
</a>

102-105: Consider using optional chaining.

Using processedEvent.description?.split('\n') prevents accidental runtime errors if description is undefined or null, though the current conditional check also works.

- {processedEvent.description &&
-   processedEvent.description.split('\n').map(...)}
+ {processedEvent.description?.split('\n')?.map(...)}
🧰 Tools
🪛 Biome (1.9.4)

[error] 102-105: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/components/content/courses/MobileCourseDetails.js (1)

86-89: Leverage optional chaining for readability.

Replacing processedEvent.description && processedEvent.description.split('\n') with optional chaining can simplify the expression.

- {processedEvent.description &&
-   processedEvent.description.split('\n').map(...)}
+ {processedEvent.description?.split('\n')?.map(...)}
🧰 Tools
🪛 Biome (1.9.4)

[error] 86-89: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

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

117-143: Loading state consideration.

Currently, the brand is displayed while the course data loads. You may consider a short loading indicator or skeleton to avoid flickering if course takes time to resolve.

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

8-9: Unused CourseHeader import.
CourseHeader is imported but never referenced. If it’s no longer needed, please remove it to keep the code clean and avoid confusion.

 import CourseSidebar from '@/components/content/courses/CourseSidebar';
-import CourseHeader from '@/components/content/courses/CourseHeader';

183-192: Storing nAddress from the router.
This useEffect properly checks if the slug is an naddr and sets nAddress. Consider verifying if slug might be undefined or empty before calling slug.includes(...) to avoid potential errors.

  if (router.isReady && slug) {
    if (slug.includes('naddr')) {
      setNAddress(slug);
    }
  }

256-265: handleLessonSelect for lesson transitions and shallow routing.
This properly updates the URL's active param and toggles the content tab in mobile view. You may consider scrolling the page to top on lesson select for improved user experience.

const handleLessonSelect = (index) => {
  setActiveIndex(index);
  router.push(...);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
  ...
};

318-345: getTabItems for building the MenuTab list.
This function properly adds “Lessons” on mobile only. If the tab structure grows more complex, consider a single source of truth for both getTabItems and toggleTab.


347-365: Keyboard navigation for tab switching.
Attaching listeners for left/right arrow keys is great for accessibility. Consider adding Home and End key handling.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 24e8a51 and a6b442c.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (22)
  • package.json (2 hunks)
  • src/components/content/combined/CombinedDetails.js (3 hunks)
  • src/components/content/courses/CombinedLesson.js (3 hunks)
  • src/components/content/courses/CourseDetails.js (4 hunks)
  • src/components/content/courses/CourseHeader.js (1 hunks)
  • src/components/content/courses/CourseLesson.js (3 hunks)
  • src/components/content/courses/CourseSidebar.js (1 hunks)
  • src/components/content/courses/DesktopCourseDetails.js (1 hunks)
  • src/components/content/courses/DocumentLesson.js (3 hunks)
  • src/components/content/courses/DraftCourseDetails.js (2 hunks)
  • src/components/content/courses/DraftCourseLesson.js (2 hunks)
  • src/components/content/courses/MobileCourseDetails.js (1 hunks)
  • src/components/content/courses/VideoLesson.js (3 hunks)
  • src/components/content/documents/DocumentDetails.js (3 hunks)
  • src/components/content/videos/VideoDetails.js (3 hunks)
  • src/components/markdown/MarkdownDisplay.js (1 hunks)
  • src/components/menutab/MenuTab.js (2 hunks)
  • src/components/navbar/Navbar.js (3 hunks)
  • src/components/profile/DataTables/UserProgressTable.js (0 hunks)
  • src/config/appConfig.js (1 hunks)
  • src/pages/course/[slug]/index.js (7 hunks)
  • src/pages/draft/[slug]/index.js (2 hunks)
💤 Files with no reviewable changes (1)
  • src/components/profile/DataTables/UserProgressTable.js
🧰 Additional context used
🧬 Code Graph Analysis (17)
src/components/content/courses/DraftCourseDetails.js (1)
src/components/markdown/MarkdownDisplay.js (1)
  • MarkdownDisplay (8-25)
src/components/content/courses/DocumentLesson.js (1)
src/components/markdown/MarkdownDisplay.js (1)
  • MarkdownDisplay (8-25)
src/components/content/courses/CourseDetails.js (2)
src/components/content/courses/MobileCourseDetails.js (1)
  • MobileCourseDetails (9-135)
src/components/content/courses/DesktopCourseDetails.js (1)
  • DesktopCourseDetails (9-151)
src/components/content/courses/DraftCourseLesson.js (1)
src/components/markdown/MarkdownDisplay.js (1)
  • MarkdownDisplay (8-25)
src/components/content/videos/VideoDetails.js (2)
src/components/markdown/MarkdownDisplay.js (1)
  • MarkdownDisplay (8-25)
src/pages/details/[slug]/index.js (1)
  • decryptedContent (22-22)
src/pages/draft/[slug]/index.js (3)
src/pages/draft/[slug]/edit.js (1)
  • draft (11-11)
src/pages/course/[slug]/draft/edit.js (1)
  • draft (10-10)
src/components/markdown/MarkdownDisplay.js (1)
  • MarkdownDisplay (8-25)
src/components/content/combined/CombinedDetails.js (2)
src/components/markdown/MarkdownDisplay.js (1)
  • MarkdownDisplay (8-25)
src/pages/details/[slug]/index.js (1)
  • decryptedContent (22-22)
src/components/menutab/MenuTab.js (2)
src/pages/course/[slug]/index.js (2)
  • isMobileView (179-179)
  • sidebarVisible (176-176)
src/components/buttons/GenericButton.js (1)
  • GenericButton (4-35)
src/components/content/courses/CourseSidebar.js (5)
src/components/content/courses/CourseDetails.js (1)
  • useImageProxy (39-39)
src/pages/course/[slug]/index.js (5)
  • navbarHeight (181-181)
  • activeIndex (174-174)
  • isMobileView (179-179)
  • completedLessons (175-175)
  • lessons (91-91)
src/components/navbar/Navbar.js (1)
  • navbarHeight (19-19)
src/hooks/useImageProxy.js (1)
  • returnImageProxy (6-26)
src/components/buttons/GenericButton.js (1)
  • GenericButton (4-35)
src/components/content/courses/CourseLesson.js (1)
src/components/markdown/MarkdownDisplay.js (1)
  • MarkdownDisplay (8-25)
src/components/content/courses/CourseHeader.js (5)
src/components/content/courses/CourseDetails.js (1)
  • router (38-38)
src/pages/course/[slug]/index.js (4)
  • router (170-170)
  • course (24-24)
  • isMobileView (179-179)
  • navbarHeight (181-181)
src/components/navbar/Navbar.js (3)
  • router (17-17)
  • course (25-25)
  • navbarHeight (19-19)
src/components/buttons/GenericButton.js (1)
  • GenericButton (4-35)
src/components/content/courses/CourseSidebar.js (1)
  • navbarHeight (20-20)
src/components/content/courses/CombinedLesson.js (1)
src/components/markdown/MarkdownDisplay.js (1)
  • MarkdownDisplay (8-25)
src/components/content/courses/VideoLesson.js (1)
src/components/markdown/MarkdownDisplay.js (1)
  • MarkdownDisplay (8-25)
src/components/content/courses/MobileCourseDetails.js (7)
src/components/content/courses/DesktopCourseDetails.js (1)
  • router (23-23)
src/components/content/courses/CourseDetails.js (5)
  • router (38-38)
  • zapAmount (35-35)
  • author (36-36)
  • menuItems (62-80)
  • renderPaymentMessage (126-193)
src/pages/course/[slug]/index.js (3)
  • router (170-170)
  • lessons (91-91)
  • paidCourse (26-26)
src/components/zaps/ZapDisplay.js (1)
  • ZapDisplay (7-46)
src/hooks/nostrQueries/zaps/useZapsSubscription.js (1)
  • zapsLoading (7-7)
src/hooks/useImageProxy.js (1)
  • returnImageProxy (6-26)
src/components/ui/MoreOptionsMenu.js (1)
  • MoreOptionsMenu (14-67)
src/components/content/courses/DesktopCourseDetails.js (7)
src/components/content/courses/MobileCourseDetails.js (1)
  • router (23-23)
src/components/content/courses/CourseDetails.js (5)
  • router (38-38)
  • zapAmount (35-35)
  • menuItems (62-80)
  • author (36-36)
  • renderPaymentMessage (126-193)
src/pages/course/[slug]/index.js (3)
  • router (170-170)
  • lessons (91-91)
  • paidCourse (26-26)
src/hooks/useImageProxy.js (1)
  • returnImageProxy (6-26)
src/components/zaps/ZapDisplay.js (1)
  • ZapDisplay (7-46)
src/hooks/nostrQueries/zaps/useZapsSubscription.js (1)
  • zapsLoading (7-7)
src/components/ui/MoreOptionsMenu.js (1)
  • MoreOptionsMenu (14-67)
src/components/navbar/Navbar.js (2)
src/components/content/courses/CourseHeader.js (2)
  • router (12-12)
  • CourseHeader (6-80)
src/utils/nostr.js (2)
  • parseCourseEvent (149-218)
  • parseCourseEvent (149-218)
src/pages/course/[slug]/index.js (3)
src/components/content/courses/CourseDetails.js (6)
  • nAddress (37-37)
  • windowWidth (43-43)
  • router (38-38)
  • fetchAuthor (88-99)
  • session (41-41)
  • CourseDetails (25-233)
src/components/ZapThreadsWrapper.js (1)
  • ZapThreadsWrapper (3-56)
src/components/menutab/MenuTab.js (1)
  • MenuTab (6-45)
🪛 Biome (1.9.4)
src/components/content/courses/MobileCourseDetails.js

[error] 86-89: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/components/content/courses/DesktopCourseDetails.js

[error] 102-105: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

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

14-14: Added new author public key to authorPubkeys array.

The addition of the new public key appears to be correct and properly formatted. This change extends the list of authorized authors in the application.

package.json (2)

41-41: Added github-markdown-css dependency.

Good addition. This package will provide GitHub-style formatting for rendered markdown content, which aligns with the overall refactoring of markdown display components in this PR.


54-54: Added remark-breaks dependency.

This plugin enables proper handling of line breaks in markdown content. It works well with the new MarkdownDisplay component to ensure consistent rendering across the application.

src/components/content/courses/DraftCourseLesson.js (2)

9-9: Replaced dynamic import with static import of MarkdownDisplay.

This change is part of the broader refactoring to standardize markdown rendering across the application. The static import is more efficient than the previous dynamic import.


148-148: Updated markdown component and prop name.

Successfully migrated from MDDisplay with source prop to MarkdownDisplay with content prop while maintaining the same styling. This change aligns with the application-wide standardization of markdown rendering.

src/components/content/courses/DraftCourseDetails.js (2)

20-20: Replaced dynamic import with static import of MarkdownDisplay.

Good change. Using a static import instead of a dynamic one simplifies the code and improves maintainability.


466-466: Updated markdown component and prop name.

Successfully replaced MDDisplay with MarkdownDisplay and updated the prop name from source to content. This maintains consistency with other similar changes throughout the application.

src/components/content/documents/DocumentDetails.js (3)

17-17: Import refactored to use static component import.

The change from dynamically importing MDDisplay to statically importing MarkdownDisplay will improve the initial load performance and code organization.


210-210: Prop name updated for consistency across the application.

The component has been updated from MDDisplay with source prop to MarkdownDisplay with content prop, which follows the naming convention of the new component as defined in the MarkdownDisplay.js file.


236-236: Component and prop updated to use new markdown renderer.

Same pattern as above - updated to use the standardized MarkdownDisplay component with the content prop.

src/components/content/combined/CombinedDetails.js (3)

17-17: Import refactored to use static component import.

The change from dynamically importing MDDisplay to statically importing MarkdownDisplay improves code maintainability and load performance.


204-204: Prop name updated for consistency across the application.

The component has been updated from MDDisplay with source prop to MarkdownDisplay with content prop, which follows the API defined in the new component.


232-232: Component and prop updated to use new markdown renderer.

Consistent with the pattern applied throughout the codebase - updated to use the standardized MarkdownDisplay component with the content prop.

src/components/content/courses/DocumentLesson.js (3)

16-16: Import refactored to use static component import.

The change from dynamically importing MDDisplay to statically importing MarkdownDisplay improves code organization and bundle management.


117-117: Prop name updated for consistency across the application.

The component has been updated from MDDisplay with source prop to MarkdownDisplay with content prop, maintaining consistency with the new markdown rendering implementation.


132-132: Component and prop updated to use new markdown renderer.

Consistent implementation of the new markdown rendering approach using MarkdownDisplay with the content prop.

src/components/content/videos/VideoDetails.js (3)

17-17: Import refactored to use static component import.

The change from dynamically importing MDDisplay to statically importing MarkdownDisplay simplifies the code and improves load performance.


207-207: Prop name updated for consistency across the application.

The component has been updated from MDDisplay with source prop to MarkdownDisplay with content prop, which aligns with the naming convention used in the new component.


240-240: Component and prop updated to use new markdown renderer.

Continuing the pattern applied throughout the codebase - updated to use the standardized MarkdownDisplay component with the content prop.

src/components/menutab/MenuTab.js (4)

3-4: Import statements properly added for the sidebar toggle button.

The Button import from PrimeReact and GenericButton from the local components directory are correctly added to support the new sidebar toggle functionality.


6-6: Well-structured function signature update.

The component now accepts three additional props (sidebarVisible, onToggleSidebar, and isMobileView) with a sensible default value for isMobileView. This enables the sidebar toggle functionality and responsive behavior.


8-8: Appropriate positioning class added.

Adding the relative class to the wrapper div is necessary to properly position the absolute-positioned sidebar toggle button that's being added.


25-42: Clean implementation of the sidebar toggle button.

The sidebar toggle button implementation:

  • Is correctly conditional based on viewport size
  • Uses appropriate positioning
  • Has dynamic icon and tooltip based on sidebar state
  • Includes proper accessibility attributes
  • Maintains clean separation of concerns

The code is well-structured and follows good UI practices.

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

1-7: Proper imports for Markdown rendering.

The component imports all necessary dependencies for enhanced Markdown rendering:

  • ReactMarkdown for core functionality
  • rehypeRaw to support raw HTML
  • remarkGfm for GitHub Flavored Markdown
  • remarkBreaks for better line break handling
  • GitHub markdown CSS for consistent styling

This provides a comprehensive solution for Markdown rendering.


8-25: Well-implemented Markdown renderer with safety features.

This implementation:

  • Handles null content gracefully
  • Uses appropriate plugins for enhanced markdown features
  • Makes external links secure with target="_blank" and rel="noopener noreferrer"
  • Applies consistent image styling
  • Properly handles className extension for customization

The component effectively centralizes Markdown rendering logic that was previously scattered across the application.


27-27: Export implemented correctly.

The component is properly exported as the default export.

src/components/content/courses/CourseHeader.js (4)

1-11: Well-structured component declaration with appropriate imports.

The component imports necessary dependencies and defines a clean function signature with sensible default values. The isNavbarMode prop with default false allows for versatile rendering in different contexts.


14-25: Smart navigation handling with context awareness.

The handleBackNavigation function intelligently determines where to navigate based on the current context:

  • If viewing a specific lesson, it returns to the course overview
  • If viewing the course overview, it returns to the home page

This creates an intuitive user experience with context-aware back navigation.


27-55: Clean implementation of navbar mode with event handling.

The navbar mode implementation:

  • Conditionally renders based on the isNavbarMode prop
  • Has proper event propagation control in the nested button
  • Uses appropriate styling for compact display
  • Implements truncation for long course names
  • Has responsive text sizing based on viewport

The component effectively serves as a compact course header for the navbar.


58-79: Well-implemented standard mode with proper positioning.

The standard mode implementation:

  • Uses sticky positioning with appropriate z-index
  • Properly offsets from the navbar using the provided height
  • Implements appropriate styling and spacing
  • Has responsive text sizing
  • Includes accessible controls with tooltips

The component provides a consistent header experience for course content.

src/components/content/courses/CombinedLesson.js (3)

17-17: Import updated from dynamic to static.

The component now directly imports MarkdownDisplay instead of using dynamic import for MDDisplay. This simplifies the code and aligns with the broader refactoring effort across the application.


174-174: Component and prop names updated consistently.

The MDDisplay component has been replaced with MarkdownDisplay and the prop name has been updated from source to content while maintaining the same className for styling.


216-216: Component and prop names updated consistently.

The same component and prop name updates have been applied consistently in this second instance, ensuring a uniform implementation throughout the file.

src/components/content/courses/CourseLesson.js (1)

15-15: Direct import of MarkdownDisplay.

Switching from a dynamic import to a named import streamlines the rendering of Markdown content and may simplify the code. However, be mindful that this removes any potential code-splitting benefits that dynamic imports might have offered.

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

22-22: Importing MarkdownDisplay directly.

Good job replacing dynamic imports with a direct import for a cleaner approach. Keep in mind this can slightly increase initial load but provides simpler maintainability.

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

126-155:

Details

✅ Verification successful

Z-index and styling considerations.

The sidebar uses z-[999] which can conflict with other layered UI elements (e.g., modals or tooltips). Verify that 999 is an appropriate stacking context for your layout. Also, watch for consistent application of top offsets (navbarHeight + 70) to avoid layout shifts.


🏁 Script executed:

#!/bin/bash
# Searching the codebase for other z-index values that might conflict
rg -A 2 'z-\['

Length of output: 635


Stacking Context Consistency and Offset Confirmation

  • The use of z-[999] in both CourseSidebar.js and src/pages/course/[slug]/index.js shows that the stacking context was intentionally unified across these components.
  • Please ensure that this z-index value is sufficient to handle any higher-layer elements (e.g., modals or tooltips) that might appear in the future.
  • Also, verify that the top offset calculation (navbarHeight + 70px) remains consistent with header spacing changes to prevent layout shifts.
src/components/content/courses/CourseDetails.js (3)

20-23: Clear labeling for imported components.

Great job introducing a clear comment block (// Import the desktop and mobile components). This improves readability and makes it evident which layout components are being used.


203-218: Good DRY approach with shared props.

Creating a detailsProps object centralizes the data passed to both desktop and mobile views. This is a neat approach to reduce duplication between the two layouts.


220-229: Conditional rendering of layout components.

Pulling the large render logic into separate MobileCourseDetails and DesktopCourseDetails components increases maintainability. This approach also seems to align well with your isPhone check.

src/components/content/courses/VideoLesson.js (2)

16-16: Confirm SSR compatibility when removing dynamic import.

Previously, the VideoLesson component used dynamic(() => import(...), { ssr: false }). Switching to a direct import may cause SSR issues if MarkdownDisplay references the browser environment. Please verify it is safe for SSR in all usage contexts.


169-169: Ensure content is properly sanitized.

rehypeRaw processes raw HTML, which can open the door to XSS if lesson.content is untrusted. Consider using a sanitizing library or removing rehypeRaw unless you intentionally want to render arbitrary HTML.

src/components/content/courses/DesktopCourseDetails.js (1)

1-22: File-level overview check.

This entire file is newly added. No syntax or critical logic issues are observed in imports and declarations. Looks good overall!

src/components/content/courses/MobileCourseDetails.js (1)

1-8: File-level overview check.

This mobile-specific component structure appears consistent with the desktop version. No obvious logic or performance concerns.

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

11-14: New imports appear valid.

Imports for CourseHeader, useNDKContext, nip19, and parseCourseEvent look correct and are used consistently.


24-30: Track course state in the navbar.

Storing course and isCompleted here helps provide a contextual navbar. Check if the approach is consistent with the overall app architecture.


32-68: Evaluate multiple event results & array type.

When calling ndk.fetchEvent({ '#d': [identifier] }), ensure it’s guaranteed to return a single event. Also confirm completedCourses is always an array to avoid unexpected issues with includes().

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

1-1: No issues with the added React hooks import.
You're now importing useMemo, which is used later for isCourseCompleted. This looks correct.


12-12: Added nip19 import is appropriate.
Importing nip19 for decoding and encoding addresses is valid. Looks good.


17-17: ZapThreadsWrapper import.
This import is consistent with its usage in the QA/comments section. No issues.


18-21: New hooks and components imports.
These imports (useWindowWidth, MenuTab, Tag, MarkdownDisplay) align with usage for responsive layouts, tab navigation, tagging, and Markdown rendering. Everything seems consistent.


174-181: New state variables introduced for navigation and UI control.
Defining activeIndex, sidebarVisible, nAddress, isMobileView, and activeTab helps manage tabs and layout responsiveness. All look fine. Just ensure that CourseHeader is truly unused before keeping that import.


194-210: Synchronizing active lesson and sidebar behavior.
This useEffect sets the active lesson from the URL query and adjusts the tab/view accordingly. Looks correct. Consider falling back to 0 if parseInt fails. Currently, if active is invalid, it could become NaN.

const parsedIndex = parseInt(active, 10);
if (!isNaN(parsedIndex)) {
  setActiveIndex(parsedIndex);
  setActiveTab('content');
} else {
  setActiveIndex(0);
  setActiveTab('overview');
}

232-232: Empty line insertion.
This spacing change has no functional effect.


239-239: Empty line insertion.
No impact on functionality.


248-254: isCourseCompleted logic.
You're using useMemo to determine course completion if at least one lesson is completed. Ensure this meets the desired definition of “course completion.” This is a valid approach if partial completion is acceptable.


284-302: Tab switching logic with dynamic insertion of “lessons” for mobile.
Conditionally adding "lessons" in the middle of the tabMap is a neat approach. Just note that if more tabs are added later, the insertion index might require maintenance.


304-306: Simple handleToggleSidebar.
Toggles the boolean correctly. Straightforward and functional.


392-431: renderOverviewSection
Neatly encapsulates the display for mobile vs. desktop. The usage of <CourseDetails> with showCompletedTag ensures consistent styling. Looks good.


452-452: Minor change at line 452.
No functional changes. Possibly just formatting.


478-494: Sticky tab navigation UI.
Ensuring the MenuTab is pinned below the navbarHeight with a border for visual delineation is a nice detail. The approach is clear and straightforward.


496-529: Conditional display of overview, content, and QA.
The usage of block/hidden classes for tab switching is fine. Alternatively, you could map each tab to a small component for clarity, but this is acceptable.


531-559: Desktop sidebar toggling.
Positioning the sidebar absolutely on desktop devices and adjusting translation is performed correctly. Good job.


560-584: Mobile sidebar toggling and lesson selection.
Dynamic switching between tabs and toggling the sidebar ensures a decent mobile experience. No major issues identified.

}
if (lesson?.content) {
return <MDDisplay className="p-4 rounded-lg w-full" source={lesson.content} />;
return <MarkdownDisplay content={lesson.content} className="p-4 rounded-lg w-full" />;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Repeated raw HTML rendering concern.

As with the previous usage, ensure lesson.content is sanitized or from a trusted source. The same recommendation applies here to mitigate XSS risks.

const renderContent = () => {
if (isPaid && decryptionPerformed) {
return <MDDisplay className="p-4 rounded-lg w-full" source={lesson.content} />;
return <MarkdownDisplay content={lesson.content} className="p-4 rounded-lg w-full" />;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential XSS risk with raw HTML.

Although MarkdownDisplay enhances readability, consider sanitizing lesson.content if it comes from untrusted sources. The rehypeRaw plugin processes raw HTML and could open the door for XSS attacks.

- return <MarkdownDisplay content={lesson.content} className="p-4 rounded-lg w-full" />;
+ // Recommendation: Apply a sanitize plugin or ensure you trust the source before using rehypeRaw

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

</div>
<div className="w-[75vw] mx-auto mt-12 p-12 border-t-2 border-gray-300 max-tab:p-0 max-mob:p-0 max-tab:max-w-[100vw] max-mob:max-w-[100vw]">
{draft?.content && <MDDisplay className="p-4 rounded-lg" source={draft.content} />}
{draft?.content && <MarkdownDisplay content={draft.content} className="p-4 rounded-lg" />}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Check for XSS vulnerabilities when displaying raw HTML.

If draft.content can contain user-generated or untrusted content, confirm that it is sanitized or that only trusted HTML is allowed.

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

🧹 Nitpick comments (3)
src/components/content/courses/CourseSidebar.js (3)

7-17: Remove or Integrate the onClose prop.
The onClose prop is never used in this component. You can either wire it into the closing logic or remove it to avoid confusion.


19-19: Avoid hardcoding navbarHeight.
Extract the 60 value from a shared config or constant to keep the code more maintainable and consistent across components.


117-143: Consider unifying spacing offsets.
Currently, the top offset is set to navbarHeight + 70. If possible, reference a shared layout constant or context to avoid mismatched spacing across components.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 0f01404 and 14f072e.

📒 Files selected for processing (1)
  • src/components/content/courses/CourseSidebar.js (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/components/content/courses/CourseSidebar.js (5)
src/components/content/courses/CourseDetails.js (1)
  • useImageProxy (39-39)
src/pages/course/[slug]/index.js (6)
  • navbarHeight (181-181)
  • sidebarVisible (176-176)
  • activeIndex (174-174)
  • isMobileView (179-179)
  • completedLessons (175-175)
  • lessons (91-91)
src/components/navbar/Navbar.js (1)
  • navbarHeight (19-19)
src/hooks/useImageProxy.js (1)
  • returnImageProxy (6-26)
src/components/buttons/GenericButton.js (1)
  • GenericButton (4-35)
🔇 Additional comments (6)
src/components/content/courses/CourseSidebar.js (6)

1-6: Imports look good.


21-26: Toggle function is clean and correct.
This logic properly relies on the parent’s sidebarVisible state without duplicating local state.


28-73: Verify lesson object structure.
completedLessons.includes(lesson.id) implies each lesson must have a valid id. Ensure you pass lessons with valid IDs to prevent unexpected errors.


75-99: Sidebar content component looks solid.
The close button logic for desktop mode is concise and user-friendly.


101-115: Mobile lessons tab is well-structured.
Rendering lessons in this separate layout is a clean solution for smaller screens.


145-145: Export statement is straightforward.

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/pages/course/[slug]/index.js (1)

95-122: 🛠️ Refactor suggestion

Batch lesson fetches to reduce re-renders

useLessons fires ndk.fetchEvent for each lesson ID and calls setLessons inside each promise, causing n re-renders. Fetch all in parallel and set state once to improve performance.

-lessonIds.forEach(lessonId => fetchLesson(lessonId));
+Promise.all(lessonIds.map(fetchLesson)).then(() => {/* optionally sort here */});
♻️ Duplicate comments (1)
src/pages/course/[slug]/index.js (1)

411-421: Relay URL string duplication persists
The hard-coded relay list appears twice even after the earlier refactor suggestion. Extract it to a constant to stay DRY.

🧹 Nitpick comments (7)
src/components/content/courses/MobileCourseDetails.js (3)

23-24: Unused router variable & import

useRouter is imported and const router = useRouter() is executed but router is never referenced afterwards. This adds unnecessary bundle size and re-renders when the route changes.

-import { useRouter } from 'next/router';
+// import { useRouter } from 'next/router';   // not needed
@@
-  const router = useRouter();
+  // const router = useRouter();            // remove

31-35: Avoid using index as React list key

index keys break React’s reconciliation when the array order changes. The topic string itself is unique and stable – prefer that.

- processedEvent.topics.map((topic, index) => (
-   <Tag className="text-white" key={index} value={topic}></Tag>
- ))
+ processedEvent.topics.map(topic => (
+   <Tag className="text-white" key={topic} value={topic} />
+ ))

86-90: Leverage optional chaining & reduce re-render work

The double check processedEvent.description && … is safe but a bit verbose. Optional chaining is clearer and flagged by Biome.

-processedEvent.description &&
-  processedEvent.description
-    .split('\n')
-    .map((line, index) => <p key={index} className="text-sm mb-2">{line}</p>)
+processedEvent.description?.split('\n').map((line, idx) => (
+  <p key={idx} className="text-sm mb-2">{line}</p>
+))
🧰 Tools
🪛 Biome (1.9.4)

[error] 86-89: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/components/content/courses/DesktopCourseDetails.js (3)

1-5: Unused useRouter import & variable

useRouter is imported and instantiated but never used. Remove to save bytes and avoid unnecessary renders.

-import { useRouter } from 'next/router';
@@
-  const router = useRouter();
+  // router not required

63-68: Prefer stable key over array index

Using index as key has reconciliation pitfalls; use the topic value.

-processedEvent.topics.map((topic, index) => (
-  <Tag className="text-white" key={index} value={topic}></Tag>
-))
+processedEvent.topics.map(topic => (
+  <Tag className="text-white" key={topic} value={topic} />
+))

102-105: Apply optional chaining to satisfy linter & improve readability

-processedEvent.description &&
-  processedEvent.description
-    .split('\n')
-    .map((line, index) => <p key={index} className="mb-2">{line}</p>)
+processedEvent.description?.split('\n').map((line, idx) => (
+  <p key={idx} className="mb-2">{line}</p>
+))
🧰 Tools
🪛 Biome (1.9.4)

[error] 102-105: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

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

48-58: Specify kinds in NDK filter for faster relay query

Fetching by '#d' only may scan multiple kinds. If you know courses are kind 30023/30402, include kinds to limit relay load and reduce latency.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 4a1675b and 55c2c34.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (40)
  • package.json (2 hunks)
  • src/components/banner/HeroBanner.js (1 hunks)
  • src/components/charts/ActivityContributionChart.js (0 hunks)
  • src/components/charts/CombinedContributionChart.js (0 hunks)
  • src/components/content/carousels/CoursesCarousel.js (1 hunks)
  • src/components/content/carousels/DocumentsCarousel.js (0 hunks)
  • src/components/content/combined/CombinedDetails.js (3 hunks)
  • src/components/content/courses/CombinedLesson.js (3 hunks)
  • src/components/content/courses/CourseDetails.js (4 hunks)
  • src/components/content/courses/CourseHeader.js (1 hunks)
  • src/components/content/courses/CourseLesson.js (3 hunks)
  • src/components/content/courses/CourseSidebar.js (1 hunks)
  • src/components/content/courses/DesktopCourseDetails.js (1 hunks)
  • src/components/content/courses/DocumentLesson.js (3 hunks)
  • src/components/content/courses/DraftCourseDetails.js (2 hunks)
  • src/components/content/courses/DraftCourseLesson.js (2 hunks)
  • src/components/content/courses/MobileCourseDetails.js (1 hunks)
  • src/components/content/courses/VideoLesson.js (3 hunks)
  • src/components/content/documents/DocumentDetails.js (3 hunks)
  • src/components/content/dropdowns/MessageDropdownItem.js (0 hunks)
  • src/components/content/lists/ContentListItem.js (1 hunks)
  • src/components/content/videos/VideoDetails.js (3 hunks)
  • src/components/feeds/MessageInput.js (1 hunks)
  • src/components/feeds/StackerNewsFeed.js (1 hunks)
  • src/components/forms/course/embedded/EmbeddedDocumentForm.js (0 hunks)
  • src/components/markdown/MarkdownDisplay.js (1 hunks)
  • src/components/menutab/CommunityMenuTab.js (1 hunks)
  • src/components/menutab/MenuTab.js (2 hunks)
  • src/components/navbar/Navbar.js (3 hunks)
  • src/components/profile/DataTables/UserProgressTable.js (0 hunks)
  • src/components/profile/UserAccountLinking.js (0 hunks)
  • src/components/profile/UserProfileCard.js (0 hunks)
  • src/components/profile/subscription/CalendlyEmbed.js (1 hunks)
  • src/components/zaps/ZapDisplay.js (1 hunks)
  • src/components/zaps/ZapForm.js (1 hunks)
  • src/config/appConfig.js (1 hunks)
  • src/context/NDKContext.js (1 hunks)
  • src/pages/_app.js (0 hunks)
  • src/pages/course/[slug]/index.js (8 hunks)
  • src/pages/draft/[slug]/index.js (2 hunks)
💤 Files with no reviewable changes (9)
  • src/components/profile/UserAccountLinking.js
  • src/components/forms/course/embedded/EmbeddedDocumentForm.js
  • src/components/profile/DataTables/UserProgressTable.js
  • src/components/profile/UserProfileCard.js
  • src/components/charts/CombinedContributionChart.js
  • src/components/content/carousels/DocumentsCarousel.js
  • src/components/charts/ActivityContributionChart.js
  • src/components/content/dropdowns/MessageDropdownItem.js
  • src/pages/_app.js
✅ Files skipped from review due to trivial changes (8)
  • src/config/appConfig.js
  • src/components/feeds/StackerNewsFeed.js
  • src/components/zaps/ZapForm.js
  • src/components/feeds/MessageInput.js
  • src/components/profile/subscription/CalendlyEmbed.js
  • src/pages/draft/[slug]/index.js
  • src/components/content/courses/VideoLesson.js
  • src/components/content/courses/CourseLesson.js
🚧 Files skipped from review as they are similar to previous changes (19)
  • src/components/menutab/CommunityMenuTab.js
  • src/context/NDKContext.js
  • src/components/content/carousels/CoursesCarousel.js
  • src/components/content/lists/ContentListItem.js
  • src/components/content/combined/CombinedDetails.js
  • src/components/content/courses/DocumentLesson.js
  • src/components/banner/HeroBanner.js
  • src/components/zaps/ZapDisplay.js
  • src/components/content/videos/VideoDetails.js
  • src/components/content/courses/DraftCourseDetails.js
  • src/components/markdown/MarkdownDisplay.js
  • src/components/content/courses/CourseHeader.js
  • src/components/content/documents/DocumentDetails.js
  • src/components/content/courses/CourseDetails.js
  • src/components/content/courses/DraftCourseLesson.js
  • package.json
  • src/components/content/courses/CourseSidebar.js
  • src/components/content/courses/CombinedLesson.js
  • src/components/menutab/MenuTab.js
🧰 Additional context used
🪛 Biome (1.9.4)
src/components/content/courses/DesktopCourseDetails.js

[error] 102-105: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/components/content/courses/MobileCourseDetails.js

[error] 86-89: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (3)
src/components/content/courses/DesktopCourseDetails.js (1)

10-14: decryptionPerformed prop appears unused

decryptionPerformed is accepted but never referenced inside this component. If not required, drop it from the signature to avoid confusion.

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

495-510: Possible mismatch between lesson classification and lesson.type

The fall-through relies on both lesson.type and lesson.topics. Ensure parseEvent sets lesson.type consistently; otherwise a lesson with topics: ['video'] but missing type would render nothing.


256-262: Course completion metric may be misleading

isCourseCompleted returns true when any lesson is completed. If full completion is intended, compare against uniqueLessons.length.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/pages/course/[slug]/index.js (2)

412-413: Extract relay URLs to a constant or configuration.

The relay URLs are still hardcoded in the component. This was previously identified in a past review and should be extracted to a constant or configuration value.

+ // Add at the top of the component or in a shared config file
+ const NOSTR_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 the component:
<ZapThreadsWrapper
  anchor={nAddress}
  user={nsec || npub || null}
- 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_RELAYS}
  disable="zaps"
  isAuthorized={isAuthorized}
/>

343-351: Refactor getActiveTabIndex to reduce duplication with toggleTab.

The tab mapping logic is duplicated in both getActiveTabIndex and toggleTab functions, which was previously noted as an issue. Consider refactoring to define the tab map once and reuse it in both functions.

+ // Define tabMap once at component level
+ const getTabMap = useCallback(() => {
+   const tabMap = ['overview', 'content', 'qa'];
+   if (isMobileView) {
+     tabMap.splice(2, 0, 'lessons');
+   }
+   return tabMap;
+ }, [isMobileView]);

  const toggleTab = (index) => {
-   const tabMap = ['overview', 'content', 'qa'];
-   // If mobile and we have the lessons tab, insert it at index 2
-   if (isMobileView) {
-     tabMap.splice(2, 0, 'lessons');
-   }
+   const tabMap = getTabMap();
    
    const tabName = tabMap[index];
    setActiveTab(tabName);
    
    // Only show/hide sidebar on mobile - desktop keeps sidebar visible
    if (isMobileView) {
      // Rest of the function...
    }
  };

  // Map active tab name back to index for MenuTab
  const getActiveTabIndex = () => {
-   const tabMap = ['overview', 'content', 'qa'];
-   if (isMobileView) {
-     tabMap.splice(2, 0, 'lessons');
-   }
+   const tabMap = getTabMap();
    
    return tabMap.indexOf(activeTab);
  };
🧹 Nitpick comments (4)
src/pages/course/[slug]/index.js (4)

604-606: Fix incorrect tab label reference.

When selecting a lesson on mobile, the code attempts to find a tab labeled 'Content' which is correct but could be more robust by using the same constant defined in the tab items.

onLessonSelect={(index) => {
  handleLessonSelect(index);
  if (isMobileView) {
-   toggleTab(getTabItems().findIndex(item => item.label === 'Content'));
+   toggleTab(1); // Content tab is always at index 1 in getTabItems()
  }
}}

514-621: Consider extracting layout components for better maintainability.

The component contains a large render method with complex conditional rendering and styling. Consider extracting parts of the UI into smaller components to improve readability and maintainability.

For example, you could create:

  • A CourseTabContent component for tab content rendering
  • A SidebarContainer component that handles the different desktop/mobile sidebar behaviors

This would make the main component more focused on orchestrating the app flow rather than handling complex UI rendering details.


216-217: Add dependency on router.query for sidebar visibility useEffect.

The useEffect hook sets sidebar visibility based on isMobileView, but should also react to changes in the router.query object to handle cases where the URL is updated directly.

useEffect(() => {
  // Auto-open sidebar on desktop, close on mobile
  setSidebarVisible(!isMobileView);
- }, [isMobileView]);
+ }, [isMobileView, router.query]);

285-290: Extract authorization logic to a function or hook.

The isAuthorized calculation is used in multiple places and could be extracted to a reusable function or custom hook for better maintainability.

+ const useAuthorization = (session, course, paidCourse) => {
+   return useMemo(() => {
+     return session?.user?.role?.subscribed || 
+       session?.user?.pubkey === course?.pubkey || 
+       !paidCourse || 
+       session?.user?.purchased?.some(purchase => purchase.courseId === course?.d);
+   }, [session, course, paidCourse]);
+ };

// Then in the component:
- const isAuthorized = 
-   session?.user?.role?.subscribed || 
-   session?.user?.pubkey === course?.pubkey || 
-   !paidCourse || 
-   session?.user?.purchased?.some(purchase => purchase.courseId === course?.d)
+ const isAuthorized = useAuthorization(session, course, paidCourse);
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 55c2c34 and d8aeec9.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (2)
  • src/config/appConfig.js (1 hunks)
  • src/pages/course/[slug]/index.js (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/config/appConfig.js
🔇 Additional comments (3)
src/pages/course/[slug]/index.js (3)

383-400: Improve keyboard navigation accessibility.

The keyboard navigation for tabs has been implemented as requested, which is great. Consider adding ARIA roles and additional keyboard support for improved accessibility.

The implementation of keyboard navigation for tabs is now in place, making the interface more accessible to keyboard users. Good job on implementing this accessibility improvement!


1-21: Good component organization with appropriate imports.

The imports are well-organized, and the new components like CourseSidebar, CourseHeader, and MarkdownDisplay improve modularity and reusability.

The refactoring to use the new MarkdownDisplay component instead of dynamic imports is a good architectural improvement.


531-564: Well-implemented tab content rendering with conditional display.

The main content area is well-structured with proper conditional rendering for different tabs and responsive layout considerations.

The use of conditional class application (${activeTab === 'overview' ? 'block' : 'hidden'}) provides a clean way to toggle tab content visibility.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/pages/course/[slug]/index.js (1)

319-337: 🛠️ Refactor suggestion

Refactor tab mapping logic to prevent duplication

The tab mapping logic is duplicated in both toggleTab and getActiveTabIndex (lines 344-351), which can lead to maintenance issues if tab order changes.

+// Define tab map once at the component level
+const getTabMap = useCallback(() => {
+  const tabMap = ['overview', 'content', 'qa'];
+  if (isMobileView) {
+    tabMap.splice(2, 0, 'lessons');
+  }
+  return tabMap;
+}, [isMobileView]);

const toggleTab = (index) => {
-  const tabMap = ['overview', 'content', 'qa'];
-  // If mobile and we have the lessons tab, insert it at index 2
-  if (isMobileView) {
-    tabMap.splice(2, 0, 'lessons');
-  }
+  const tabMap = getTabMap();
  const tabName = tabMap[index];
  setActiveTab(tabName);
  
  // Only show/hide sidebar on mobile - desktop keeps sidebar visible
  if (isMobileView) {
    if (tabName === 'lessons') {
      setSidebarVisible(true);
    } else {
      setSidebarVisible(false);
    }
  }
};

// Also update getActiveTabIndex to use the shared function
const getActiveTabIndex = () => {
-  const tabMap = ['overview', 'content', 'qa'];
-  if (isMobileView) {
-    tabMap.splice(2, 0, 'lessons');
-  }
+  const tabMap = getTabMap();
  return tabMap.indexOf(activeTab);
};
🧹 Nitpick comments (4)
src/components/content/courses/MobileCourseDetails.js (1)

86-90: Simplify description rendering with optional chaining

The description rendering logic can be simplified using optional chaining and nullish coalescing.

-{processedEvent.description &&
-  processedEvent.description
-    .split('\n')
-    .map((line, index) => <p key={index} className="text-sm mb-2">{line}</p>)}
+{processedEvent.description?.split('\n')
+  .map((line, index) => <p key={index} className="text-sm mb-2">{line}</p>) || null}
🧰 Tools
🪛 Biome (1.9.4)

[error] 86-89: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

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

410-415: Extract relay URLs to a constant

The relay URLs are hardcoded in this component. This should be extracted to a constant or configuration to avoid duplication and simplify maintenance.

+// Add at the top of the component or in a shared config file
+const NOSTR_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 the component:
<ZapThreadsWrapper
  anchor={nAddress}
  user={nsec || npub || null}
- 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_RELAYS}
  disable="zaps"
  isAuthorized={isAuthorized}
/>

353-380: Memoize tab items for better performance

The getTabItems function is called multiple times, including in render cycles. Consider using useMemo to prevent unnecessary recalculations.

- // Create tab items for MenuTab
- const getTabItems = () => {
-   const items = [
-     {
-       label: 'Overview',
-       icon: 'pi pi-home',
-     },
-     {
-       label: 'Content',
-       icon: 'pi pi-book',
-     }
-   ];
-   
-   // Add lessons tab only on mobile
-   if (isMobileView) {
-     items.push({
-       label: 'Lessons',
-       icon: 'pi pi-list',
-     });
-   }
-   
-   items.push({
-     label: 'Comments',
-     icon: 'pi pi-comments',
-   });
-   
-   return items;
- };

+ // Memoize tab items
+ const tabItems = useMemo(() => {
+   const items = [
+     {
+       label: 'Overview',
+       icon: 'pi pi-home',
+     },
+     {
+       label: 'Content',
+       icon: 'pi pi-book',
+     }
+   ];
+   
+   // Add lessons tab only on mobile
+   if (isMobileView) {
+     items.push({
+       label: 'Lessons',
+       icon: 'pi pi-list',
+     });
+   }
+   
+   items.push({
+     label: 'Comments',
+     icon: 'pi pi-comments',
+   });
+   
+   return items;
+ }, [isMobileView]);

Then update all references to getTabItems() to use tabItems directly.


183-187: Consider using a constant for the mobile breakpoint

The mobile viewport breakpoint is hardcoded as 968. Consider extracting this to a named constant or configuration value for better maintainability.

+ const MOBILE_BREAKPOINT = 968; // Extract to a constant
  const [sidebarVisible, setSidebarVisible] = useState(false);
  const [nAddress, setNAddress] = useState(null);
  const windowWidth = useWindowWidth();
- const isMobileView = windowWidth <= 968;
+ const isMobileView = windowWidth <= MOBILE_BREAKPOINT;
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between d8aeec9 and 261c27c.

📒 Files selected for processing (8)
  • src/components/content/courses/CombinedLesson.js (4 hunks)
  • src/components/content/courses/CourseDetails.js (4 hunks)
  • src/components/content/courses/MobileCourseDetails.js (1 hunks)
  • src/components/content/courses/VideoLesson.js (4 hunks)
  • src/components/content/videos/VideoDetails.js (4 hunks)
  • src/components/navbar/Navbar.js (3 hunks)
  • src/pages/course/[slug]/index.js (7 hunks)
  • src/pages/index.js (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • src/pages/index.js
  • src/components/content/courses/VideoLesson.js
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/components/content/videos/VideoDetails.js
  • src/components/content/courses/CombinedLesson.js
  • src/components/content/courses/CourseDetails.js
  • src/components/navbar/Navbar.js
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/pages/course/[slug]/index.js (6)
src/components/content/courses/CourseDetails.js (4)
  • nAddress (37-37)
  • windowWidth (43-43)
  • session (41-41)
  • CourseDetails (25-233)
src/hooks/useWindowWidth.js (1)
  • useWindowWidth (3-24)
src/components/content/courses/CourseSidebar.js (2)
  • navbarHeight (19-19)
  • CourseSidebar (7-143)
src/components/ZapThreadsWrapper.js (1)
  • ZapThreadsWrapper (3-92)
src/components/menutab/MenuTab.js (1)
  • MenuTab (5-44)
src/components/markdown/MarkdownDisplay.js (1)
  • MarkdownDisplay (8-25)
🪛 Biome (1.9.4)
src/components/content/courses/MobileCourseDetails.js

[error] 86-89: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (8)
src/components/content/courses/MobileCourseDetails.js (4)

8-21: Props structure looks good

The component accepts all necessary props for rendering a comprehensive mobile course view, including content details, payment status, author information, and UI control flags.


33-35: Completion tag implementation is correct

The conditional rendering logic for the completion tag properly respects the showCompletedTag prop, fixing the issue mentioned in previous reviews.


60-67: Author link is properly implemented

The author link now correctly includes the href attribute, making it functional as a navigation link to the author's profile page.


98-131: Course information section is well structured

The course information section provides a clear overview of course details with appropriate conditional rendering for paid courses and publication dates.

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

554-556: MarkdownDisplay refactoring is well implemented

The refactoring to use the new MarkdownDisplay component for rendering markdown content is well implemented and consistent with the broader application changes.


382-400: Keyboard navigation enhancement is well implemented

The implementation of keyboard navigation for tabs using arrow keys is well done and improves accessibility.


428-466: Course overview section with responsive design

The course overview section is well implemented with responsive design considerations, showing different layouts for mobile and desktop views.


603-607: ⚠️ Potential issue

Fix incorrect tab label reference

When selecting a lesson on mobile, the code attempts to find a tab with the label 'Content', but should verify this matches the exact label used in getTabItems().

onLessonSelect={(index) => {
  handleLessonSelect(index);
  if (isMobileView) {
-   toggleTab(getTabItems().findIndex(item => item.label === 'Content'));
+   toggleTab(1); // Directly use index 1 which corresponds to 'Content' tab
  }
}}

Likely an incorrect or invalid review comment.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/components/content/courses/DesktopCourseDetails.js (1)

33-33: Add null/undefined handling for returnImageProxy function calls

The returnImageProxy function is called without checking if the parameters might be undefined. Consider adding null handling or ensuring that default values are provided.

- src={returnImageProxy(processedEvent.image)}
+ src={returnImageProxy(processedEvent?.image || '')}
- src={returnImageProxy(author?.avatar, author?.pubkey)}
+ src={returnImageProxy(author?.avatar || '', author?.pubkey || '')}

Also applies to: 75-75

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 261c27c and f3f16a4.

📒 Files selected for processing (1)
  • src/components/content/courses/DesktopCourseDetails.js (1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
src/components/content/courses/DesktopCourseDetails.js

[error] 103-106: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (1)
src/components/content/courses/DesktopCourseDetails.js (1)

1-152: Overall, well-structured component with good organization

The component is well-organized with clear section comments, proper use of conditional rendering, and good accessibility practices. The layout is responsive and follows good React component design principles.

🧰 Tools
🪛 Biome (1.9.4)

[error] 103-106: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

@AustinKelsay
Copy link
Copy Markdown
Owner Author

@CodeRabbit full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 24, 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: 0

♻️ Duplicate comments (4)
src/pages/course/[slug]/index.js (4)

411-412: Extract relay URLs to a constant.

The relay URLs are hardcoded in the component. Consider extracting them to a constant or configuration to maintain DRY principles.

+ // At the top of the file with other constants
+ const NOSTR_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 the component
<ZapThreadsWrapper
  anchor={nAddress}
  user={nsec || npub || null}
- 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_RELAYS}
  disable="zaps"
  isAuthorized={isAuthorized}
/>

334-335: ⚠️ Potential issue

Fix incorrect reference to getTabMap.

There's a bug in the tab toggling logic where getTabMap is incorrectly treated as an array instead of a memoized value.

const toggleTab = (index) => {
-  const tabName = getTabMap[index];
+  const tabName = getTabMap[index];
  setActiveTab(tabName);

347-350: ⚠️ Potential issue

Inconsistent approach to accessing tab map.

The implementation of getActiveTabIndex incorrectly uses indexOf on getTabMap directly, which treats it as an array, contradicting its implementation as a memoized value.

const getActiveTabIndex = () => {
-  return getTabMap.indexOf(activeTab);
+  return getTabMap.indexOf(activeTab);
};

382-399: ⚠️ Potential issue

Fix keyboard navigation accessibility implementation.

The keyboard navigation implementation also incorrectly accesses getTabMap as an array instead of a memoized value.

useEffect(() => {
  const handleKeyDown = (e) => {
    if (e.key === 'ArrowRight') {
      const currentIndex = getActiveTabIndex();
-     const nextIndex = (currentIndex + 1) % getTabMap.length;
+     const nextIndex = (currentIndex + 1) % getTabMap.length;
      toggleTab(nextIndex);
    } else if (e.key === 'ArrowLeft') {
      const currentIndex = getActiveTabIndex();
-     const prevIndex = (currentIndex - 1 + getTabMap.length) % getTabMap.length;
+     const prevIndex = (currentIndex - 1 + getTabMap().length) % getTabMap().length;
      toggleTab(prevIndex);
    }
  };

  document.addEventListener('keydown', handleKeyDown);
  return () => {
    document.removeEventListener('keydown', handleKeyDown);
  };
}, [activeTab, getTabMap, toggleTab]);
🧹 Nitpick comments (2)
src/components/content/courses/MobileCourseDetails.js (2)

86-89: Improve robustness with optional chaining.

The description rendering code could be more resilient to handle cases where processedEvent.description might be undefined or null.

-            {processedEvent.description &&
-              processedEvent.description
-                .split('\n')
-                .map((line, index) => <p key={index} className="text-sm mb-2">{line}</p>)}
+            {processedEvent.description?.split('\n')
+              .map((line, index) => <p key={index} className="text-sm mb-2">{line}</p>)}
🧰 Tools
🪛 Biome (1.9.4)

[error] 86-89: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


27-36: Consider adding link functionality to topic tags.

The topic tags are currently displayed but not clickable. Consider making them function as links to filter courses by topic.

{processedEvent.topics &&
  processedEvent.topics.length > 0 &&
  processedEvent.topics.map((topic, index) => (
-   <Tag className="text-white" key={index} value={topic}></Tag>
+   <Tag 
+     className="text-white cursor-pointer" 
+     key={index} 
+     value={topic} 
+     onClick={() => router.push(`/courses?topic=${encodeURIComponent(topic)}`)}
+   ></Tag>
  ))}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 35639b5 and cc86708.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (41)
  • package.json (2 hunks)
  • src/components/banner/HeroBanner.js (1 hunks)
  • src/components/charts/ActivityContributionChart.js (0 hunks)
  • src/components/charts/CombinedContributionChart.js (0 hunks)
  • src/components/content/carousels/CoursesCarousel.js (1 hunks)
  • src/components/content/carousels/DocumentsCarousel.js (0 hunks)
  • src/components/content/combined/CombinedDetails.js (3 hunks)
  • src/components/content/courses/CombinedLesson.js (4 hunks)
  • src/components/content/courses/CourseDetails.js (4 hunks)
  • src/components/content/courses/CourseHeader.js (1 hunks)
  • src/components/content/courses/CourseLesson.js (3 hunks)
  • src/components/content/courses/CourseSidebar.js (1 hunks)
  • src/components/content/courses/DesktopCourseDetails.js (1 hunks)
  • src/components/content/courses/DocumentLesson.js (3 hunks)
  • src/components/content/courses/DraftCourseDetails.js (2 hunks)
  • src/components/content/courses/DraftCourseLesson.js (2 hunks)
  • src/components/content/courses/MobileCourseDetails.js (1 hunks)
  • src/components/content/courses/VideoLesson.js (4 hunks)
  • src/components/content/documents/DocumentDetails.js (3 hunks)
  • src/components/content/dropdowns/MessageDropdownItem.js (0 hunks)
  • src/components/content/lists/ContentListItem.js (1 hunks)
  • src/components/content/videos/VideoDetails.js (4 hunks)
  • src/components/feeds/MessageInput.js (1 hunks)
  • src/components/feeds/StackerNewsFeed.js (1 hunks)
  • src/components/forms/course/embedded/EmbeddedDocumentForm.js (0 hunks)
  • src/components/markdown/MarkdownDisplay.js (1 hunks)
  • src/components/menutab/CommunityMenuTab.js (1 hunks)
  • src/components/menutab/MenuTab.js (1 hunks)
  • src/components/navbar/Navbar.js (3 hunks)
  • src/components/profile/DataTables/UserProgressTable.js (0 hunks)
  • src/components/profile/UserAccountLinking.js (0 hunks)
  • src/components/profile/UserProfileCard.js (0 hunks)
  • src/components/profile/subscription/CalendlyEmbed.js (1 hunks)
  • src/components/zaps/ZapDisplay.js (1 hunks)
  • src/components/zaps/ZapForm.js (1 hunks)
  • src/config/appConfig.js (1 hunks)
  • src/context/NDKContext.js (1 hunks)
  • src/pages/_app.js (0 hunks)
  • src/pages/course/[slug]/index.js (7 hunks)
  • src/pages/draft/[slug]/index.js (2 hunks)
  • src/pages/index.js (1 hunks)
💤 Files with no reviewable changes (9)
  • src/components/profile/UserAccountLinking.js
  • src/components/content/carousels/DocumentsCarousel.js
  • src/components/forms/course/embedded/EmbeddedDocumentForm.js
  • src/pages/_app.js
  • src/components/profile/UserProfileCard.js
  • src/components/profile/DataTables/UserProgressTable.js
  • src/components/charts/CombinedContributionChart.js
  • src/components/charts/ActivityContributionChart.js
  • src/components/content/dropdowns/MessageDropdownItem.js
✅ Files skipped from review due to trivial changes (5)
  • src/components/feeds/MessageInput.js
  • src/config/appConfig.js
  • src/context/NDKContext.js
  • src/components/banner/HeroBanner.js
  • src/components/content/courses/VideoLesson.js
🚧 Files skipped from review as they are similar to previous changes (25)
  • src/components/menutab/CommunityMenuTab.js
  • src/pages/index.js
  • src/components/zaps/ZapDisplay.js
  • src/components/content/carousels/CoursesCarousel.js
  • src/components/feeds/StackerNewsFeed.js
  • src/components/content/courses/DraftCourseLesson.js
  • src/components/zaps/ZapForm.js
  • src/components/content/lists/ContentListItem.js
  • src/components/content/combined/CombinedDetails.js
  • src/components/content/courses/CourseSidebar.js
  • package.json
  • src/components/content/courses/DocumentLesson.js
  • src/components/content/documents/DocumentDetails.js
  • src/components/profile/subscription/CalendlyEmbed.js
  • src/components/content/courses/CombinedLesson.js
  • src/components/content/courses/DesktopCourseDetails.js
  • src/components/content/videos/VideoDetails.js
  • src/components/content/courses/CourseHeader.js
  • src/components/menutab/MenuTab.js
  • src/components/navbar/Navbar.js
  • src/components/markdown/MarkdownDisplay.js
  • src/pages/draft/[slug]/index.js
  • src/components/content/courses/CourseDetails.js
  • src/components/content/courses/CourseLesson.js
  • src/components/content/courses/DraftCourseDetails.js
🧰 Additional context used
🪛 Biome (1.9.4)
src/components/content/courses/MobileCourseDetails.js

[error] 86-89: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (9)
src/components/content/courses/MobileCourseDetails.js (3)

63-64: Good implementation of author link.

The anchor tag for the author name now correctly includes the href attribute, addressing a previous issue and making the link functional.


33-35: Good implementation of showCompletedTag prop.

The completed tag is now correctly conditional on both the isCompleted state and the showCompletedTag prop.


1-135: Well-implemented mobile responsive component.

This component implements a clean, mobile-optimized layout for course details with appropriate spacing, information hierarchy, and conditional rendering based on course properties.

🧰 Tools
🪛 Biome (1.9.4)

[error] 86-89: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

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

586-589: Fix lesson selection handling on mobile.

The code now correctly sets the active tab directly instead of searching for it by label, addressing a previous issue.


190-198: Good implementation of memoized tab map.

The tab map is now properly memoized with useMemo and conditionally includes the lessons tab for mobile view, improving performance and code organization.


200-213: Good handling of missing naddress.

The implementation now properly handles cases where the slug doesn't contain an naddress, showing an error toast and redirecting to the courses page after a delay.


515-527: Well-implemented tab navigation.

The tab navigation using the MenuTab component is well-structured and handles both mobile and desktop views appropriately.


531-624: Good responsive layout implementation.

The layout structure effectively handles both mobile and desktop views with appropriate sidebar behavior, fixed width calculations, and smooth transitions.


552-556: Good use of MarkdownDisplay component.

The code now uses the new MarkdownDisplay component for rendering markdown content, which aligns with the pull request's overall goal of standardizing markdown rendering.

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 (4)
src/pages/course/[slug]/index.js (4)

340-342: ⚠️ Potential issue

Fix incorrect reference to getTabMap.

There's a bug in the tab toggling logic where getTabMap is incorrectly treated as an array instead of a function.

const toggleTab = (index) => {
-  const tabName = getTabMap[index];
+  const tabName = getTabMap()[index];
  setActiveTab(tabName);

354-357: ⚠️ Potential issue

Inconsistent approach to accessing tab map.

The implementation of getActiveTabIndex inconsistently uses indexOf on getTabMap directly, which treats it as an array, contradicting its implementation as a function.

const getActiveTabIndex = () => {
-  return getTabMap.indexOf(activeTab);
+  return getTabMap().indexOf(activeTab);
};

389-406: ⚠️ Potential issue

Fix keyboard navigation implementation.

The keyboard navigation implementation also treats getTabMap as an array rather than calling it as a function.

useEffect(() => {
  const handleKeyDown = (e) => {
    if (e.key === 'ArrowRight') {
      const currentIndex = getActiveTabIndex();
-     const nextIndex = (currentIndex + 1) % getTabMap.length;
+     const nextIndex = (currentIndex + 1) % getTabMap().length;
      toggleTab(nextIndex);
    } else if (e.key === 'ArrowLeft') {
      const currentIndex = getActiveTabIndex();
-     const prevIndex = (currentIndex - 1 + getTabMap.length) % getTabMap.length;
+     const prevIndex = (currentIndex - 1 + getTabMap().length) % getTabMap().length;
      toggleTab(prevIndex);
    }
  };

  document.addEventListener('keydown', handleKeyDown);
  return () => {
    document.removeEventListener('keydown', handleKeyDown);
  };
}, [activeTab, getTabMap, toggleTab]);

415-419: 🛠️ Refactor suggestion

Extract relay URLs to a constant or configuration.

The relay URLs are hardcoded in the ZapThreadsWrapper component. Consider extracting them to a constant or configuration value to maintain consistency and avoid duplication.

+ // Add at the top of the component or in a shared config file
+ const NOSTR_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/";

<ZapThreadsWrapper
  anchor={nAddress}
  user={nsec || npub || null}
- 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_RELAYS}
  disable="zaps"
  isAuthorized={isAuthorized}
/>
🧹 Nitpick comments (1)
src/pages/course/[slug]/index.js (1)

131-136: Redundant deduplication logic.

There's duplicate deduplication logic. The code first checks for duplicates when adding lessons to the newLessons array (lines 115-118), and then does another deduplication step here using a Map.

- useEffect(() => {
-   const newUniqueLessons = Array.from(
-     new Map(lessons.map(lesson => [lesson.id, lesson])).values()
-   );
-   setUniqueLessons(newUniqueLessons);
- }, [lessons]);
+ useEffect(() => {
+   // lessons are already deduplicated in the fetchLessons function
+   setUniqueLessons(lessons);
+ }, [lessons]);

Alternatively, you could remove the deduplication in the fetchLessons function and keep only this one.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between cc86708 and fac14d5.

📒 Files selected for processing (2)
  • src/components/content/courses/DesktopCourseDetails.js (1 hunks)
  • src/pages/course/[slug]/index.js (8 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/components/content/courses/DesktopCourseDetails.js
🔇 Additional comments (6)
src/pages/course/[slug]/index.js (6)

93-128: Improved lesson fetching efficiency with batch request.

The refactored lesson fetching logic now makes a single network request with all lesson IDs instead of multiple individual requests. This is a significant performance improvement, especially for courses with many lessons.


197-205: Good use of useMemo for tab configuration.

Using useMemo for the tabMap calculation is efficient, as it only recalculates when the view mode changes.


207-220: Proper implementation of nAddress error handling.

The previously missing "no naddress" error handling has been properly implemented with error notification and redirection.


520-535: Good implementation of tab navigation.

The MenuTab implementation provides a clean and responsive navigation system with proper sidebar controls for mobile views.


561-562: Switched to new MarkdownDisplay component.

The code now uses the new MarkdownDisplay component with the updated content prop instead of the previous dynamic import for markdown rendering.


572-630: Well-structured responsive sidebar implementation.

The sidebar implementation handles both mobile and desktop views appropriately, with smooth transitions and proper visibility controls.

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