Skip to content

Conversation

@prudvinani
Copy link

@prudvinani prudvinani commented Nov 14, 2025

Fix : #155

  • Added main newsletters component with search and month filtering.
  • Created individual newsletter pages displaying detailed content.
  • Implemented reusable components for newsletter cards, filters, and empty states.
  • Introduced utility functions for filtering and grouping newsletters by month.
  • Added sample newsletter data for testing and display purposes.
Screenshot 2025-11-14 at 4 40 57 PM
opensox.mp4

Summary by CodeRabbit

Release Notes

New Features

  • Added a new Newsletters section to the dashboard, accessible from the main navigation.
  • Implemented search functionality to find newsletters by title, excerpt, or author.
  • Added monthly filtering to view newsletters from specific time periods.
  • Introduced individual newsletter detail pages with rich formatting support, including code blocks, tables, and embedded images.
  • Display newsletter metadata including publication date, read time, and author information.

- Added main newsletters component with search and month filtering.
- Created individual newsletter pages displaying detailed content.
- Implemented reusable components for newsletter cards, filters, and empty states.
- Introduced utility functions for filtering and grouping newsletters by month.
- Added sample newsletter data for testing and display purposes.
@vercel
Copy link

vercel bot commented Nov 14, 2025

@prudvinani is attempting to deploy a commit to the AJEET PRATAP SINGH's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 14, 2025

Walkthrough

Introduces a complete newsletters feature for the dashboard, including pages and components for browsing newsletters with search and month filtering, a detail page for viewing individual newsletters, utility functions for grouping and date formatting, sample newsletter data with rich content support, new UI components (Input and Select), sidebar navigation integration, and newsletter type definitions.

Changes

Cohort / File(s) Summary
Newsletter Pages & Content
apps/web/src/app/(main)/dashboard/newsletters/page.tsx, apps/web/src/app/(main)/dashboard/newsletters/Content.tsx, apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx
Main newsletters page, dashboard content component with client-side filtering (search and month), and detail page for individual newsletter viewing with formatted metadata and rich content rendering.
Newsletter Components
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx, apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx, apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterEmptyState.tsx, apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterFilters.tsx, apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterList.tsx
Reusable components for newsletter cards with hover effects, rich content rendering (paragraphs, headings, lists, code blocks, tables, links), empty state messaging, search/month filter controls, and month-grouped newsletter lists.
Newsletter Utilities
apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts, apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts
Filtering logic for search queries and month selection; grouping, sorting, and date formatting utilities for newsletters.
Newsletter Data & Types
apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts, apps/web/src/types/newsletter.ts, apps/web/src/types/index.ts
Sample newsletter data array with rich content structure; Newsletter type definition with support for code blocks and tables; type re-export from central types index.
UI Components
apps/web/src/components/ui/input.tsx, apps/web/src/components/ui/select.tsx
New reusable Input and Select components built on Radix UI primitives with styled subcomponents and consistent styling.
Configuration & Dependencies
apps/web/next.config.js, apps/web/package.json
Added Unsplash remote image pattern; added @radix-ui/react-select dependency.
Dashboard Integration
apps/web/src/components/dashboard/Sidebar.tsx
Added Newsletters route to sidebar with NewspaperIcon.

Sequence Diagram

sequenceDiagram
    actor User
    participant NewslettersPage as Newsletters Page
    participant Filters as NewsletterFilters
    participant List as NewsletterList
    participant Card as NewsletterCard
    participant Detail as Newsletter Detail

    User->>NewslettersPage: Visit /newsletters
    NewslettersPage->>NewslettersPage: Load all newsletters
    
    rect rgb(200, 220, 240)
    Note over NewslettersPage,Card: Browsing & Filtering Flow
    NewslettersPage->>Filters: Render with initial state
    User->>Filters: Search or select month
    Filters->>NewslettersPage: onSearchChange / onMonthChange
    NewslettersPage->>NewslettersPage: filterNewsletters(query, month)
    NewslettersPage->>List: Pass filtered newsletters
    List->>List: Group by month & sort
    List->>Card: Render NewsletterCard per item
    end

    rect rgb(240, 220, 200)
    Note over Card,Detail: Detail View Flow
    User->>Card: Click newsletter card
    Card->>Detail: Navigate to /newsletters/[id]
    Detail->>Detail: Load newsletter content
    Detail->>Detail: formatNewsletterDate()
    Detail->>Detail: Render NewsletterContent
    Detail->>User: Display formatted newsletter
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Newsletter filtering logic — Verify search query matching across multiple fields and case-insensitive handling; confirm month formatting consistency with Intl.DateTimeFormat
  • Rich content rendering — Review NewsletterContent component's URL detection regex and rendering of various block types (code, tables, links with external targets)
  • Radix UI Select integration — Ensure proper props forwarding, styling consistency, and accessibility attributes
  • Type definitions — Confirm Newsletter and NewsletterContentItem types are correctly referenced across components and utilities

Possibly related PRs

Poem

🐰 Newsletters bloom in the warren's den,
Filtered by month, searched again and again,
Rich content renders with code and with tables,
A cozy dashboard—tales from fable's stables.
Hops through the detail, then back once more—
📰 Browse and read what rabbits adore!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: implementing a complete newsletters dashboard feature with filtering capabilities and individual newsletter detail views.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

@cla-assistant
Copy link

cla-assistant bot commented Nov 14, 2025

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (11)
apps/web/src/app/(main)/dashboard/newsletters/page.tsx (2)

8-8: Consider removing the type assertion if data conforms to Newsletter[].

The type assertion as Newsletter[] may be unnecessary if the newsletters data already matches the Newsletter[] type. TypeScript should infer the type correctly if the data structure is aligned.

If the assertion is needed due to type mismatch, verify the data structure in data/newsletters.ts aligns with the Newsletter interface.


7-9: Consider removing the wrapper div.

The wrapper <div> doesn't add semantic value or styling. You can return the Newsletters component directly.

 export default function NewslettersPage() {
-  return (
-    <div>
-      <Newsletters newsletters={newsletters as Newsletter[]} />
-    </div>
-  );
+  return <Newsletters newsletters={newsletters as Newsletter[]} />;
 }
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterList.tsx (1)

16-24: Consider reusing NewsletterEmptyState component.

The empty state message "No newsletters yet. Check back soon!" is duplicated here and in the NewsletterEmptyState component. For consistency and maintainability, consider reusing the NewsletterEmptyState component.

  if (newsletters.length === 0) {
-    return (
-      <div className="text-center py-12">
-        <p className="text-muted-foreground">
-          No newsletters yet. Check back soon!
-        </p>
-      </div>
-    );
+    return <NewsletterEmptyState hasActiveFilters={false} onClearFilters={() => {}} />;
  }

However, verify if this component's empty state should differ from the filter-driven empty state in the parent component.

apps/web/src/types/newsletter.ts (2)

36-36: Consider the date field type.

The date field is typed as string. While this is suitable for serialization and JSON compatibility, consider whether you need:

  • A more specific type like ${number}-${number}-${number} for ISO date strings
  • Date parsing validation utilities if consuming this from external sources
  • Documentation about the expected format (appears to be "YYYY-MM-DD" based on the sample data)

The current approach is acceptable for static data and JSON serialization. This is just a consideration for future enhancements if date validation or stronger typing becomes necessary.


9-11: Consider supporting additional heading levels.

The heading type currently supports levels 1-3. Consider whether you need to support h4-h6 (level: 1 | 2 | 3 | 4 | 5 | 6) for more granular content hierarchy in future newsletters.

If the current levels (1-3) cover all use cases, the current implementation is fine.

apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx (1)

101-101: Remove redundant type assertion.

The content property is already typed as NewsletterContentItem[] in the Newsletter interface, making the as NewsletterContentItem[] assertion unnecessary.

Apply this diff:

-          <NewsletterContent content={newsletter.content as NewsletterContentItem[]} />
+          <NewsletterContent content={newsletter.content} />
apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts (2)

9-20: Remove unnecessary optional chaining on required properties.

Based on the NewsletterContentItem type definition, the content, text, and url properties are required (not optional) for their respective item types. The optional chaining operators on lines 11, 15, and 16 are unnecessary.

Apply this diff:

   const matchesContent = newsletter.content?.some((item: NewsletterContentItem) => {
     if (item.type === "paragraph" || item.type === "heading" || item.type === "bold") {
-      return item.content?.toLowerCase().includes(query);
+      return item.content.toLowerCase().includes(query);
     }
     if (item.type === "link") {
       return (
-        item.text?.toLowerCase().includes(query) ||
-        item.url?.toLowerCase().includes(query)
+        item.text.toLowerCase().includes(query) ||
+        item.url.toLowerCase().includes(query)
       );
     }
     return false;
   });

22-22: Remove redundant || false in boolean expression.

Since both matchesBasicFields and matchesContent are already boolean values, the || false is redundant.

Apply this diff:

-  return matchesBasicFields || matchesContent || false;
+  return matchesBasicFields || matchesContent;
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx (1)

18-18: Replace hardcoded color with a theme variable.

The hardcoded color #693dab should be replaced with a CSS variable or Tailwind theme color for better maintainability and theme consistency.

Consider using a semantic color like hover:border-primary or defining this color in your Tailwind theme configuration:

-      <Card className="overflow-hidden hover:shadow-lg transition-all duration-300 border-border hover:border-[#693dab] cursor-pointer">
+      <Card className="overflow-hidden hover:shadow-lg transition-all duration-300 border-border hover:border-primary cursor-pointer">
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx (2)

5-5: Remove unused import.

The Image component from next/image is imported but never used in this file.

Apply this diff:

 import { NewsletterContentItem } from "@/types/newsletter";
 import Link from "next/link";
-import Image from "next/image";

23-37: Wrap variable declarations in block scope.

Variable declarations in switch cases should be wrapped in blocks to prevent them from being erroneously accessible in other cases.

Based on learnings

Apply this diff:

           case "heading":
+            {
             const HeadingTag = `h${item.level}` as keyof JSX.IntrinsicElements;
             const headingClasses = {
             };
             return (
               <HeadingTag
                 key={index}
                 className={headingClasses[item.level]}
               >
                 {item.content}
               </HeadingTag>
             );
+            }

Apply the same pattern to the "list" case on line 71.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5445fd9 and 34e4b80.

📒 Files selected for processing (16)
  • apps/web/src/app/(main)/dashboard/newsletters/Content.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterEmptyState.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterFilters.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterList.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/page.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts (1 hunks)
  • apps/web/src/components/dashboard/Sidebar.tsx (2 hunks)
  • apps/web/src/components/ui/input.tsx (1 hunks)
  • apps/web/src/components/ui/select.tsx (1 hunks)
  • apps/web/src/types/index.ts (1 hunks)
  • apps/web/src/types/newsletter.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (11)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx (2)
apps/web/src/types/newsletter.ts (1)
  • Newsletter (33-42)
apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts (1)
  • formatNewsletterDate (51-57)
apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts (1)
apps/web/src/types/newsletter.ts (2)
  • Newsletter (33-42)
  • NewsletterContentItem (3-31)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterList.tsx (3)
apps/web/src/types/newsletter.ts (1)
  • Newsletter (33-42)
apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts (2)
  • groupByMonth (3-26)
  • sortMonthKeys (28-34)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx (1)
  • NewsletterCard (13-64)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx (1)
apps/web/src/types/newsletter.ts (1)
  • NewsletterContentItem (3-31)
apps/web/src/app/(main)/dashboard/newsletters/page.tsx (3)
apps/web/src/app/(main)/dashboard/newsletters/Content.tsx (1)
  • Newsletters (16-73)
apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts (1)
  • newsletters (2-212)
apps/web/src/types/newsletter.ts (1)
  • Newsletter (33-42)
apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx (4)
apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts (1)
  • newsletters (2-212)
apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts (1)
  • formatNewsletterDate (51-57)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx (1)
  • NewsletterContent (11-114)
apps/web/src/types/newsletter.ts (1)
  • NewsletterContentItem (3-31)
apps/web/src/app/(main)/dashboard/newsletters/Content.tsx (6)
apps/web/src/types/newsletter.ts (1)
  • Newsletter (33-42)
apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts (1)
  • getAvailableMonths (37-48)
apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts (1)
  • filterNewsletters (40-59)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterFilters.tsx (1)
  • NewsletterFilters (24-82)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterEmptyState.tsx (1)
  • NewsletterEmptyState (10-28)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterList.tsx (1)
  • NewsletterList (12-44)
apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts (2)
apps/web/src/types/newsletter.ts (1)
  • Newsletter (33-42)
apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts (1)
  • newsletters (2-212)
apps/web/src/components/ui/input.tsx (1)
apps/web/src/lib/utils.ts (1)
  • cn (4-6)
apps/web/src/components/ui/select.tsx (1)
apps/web/src/lib/utils.ts (1)
  • cn (4-6)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterFilters.tsx (2)
apps/web/src/components/ui/input.tsx (1)
  • Input (22-22)
apps/web/src/components/ui/select.tsx (5)
  • Select (149-149)
  • SelectTrigger (152-152)
  • SelectValue (151-151)
  • SelectContent (153-153)
  • SelectItem (155-155)
🪛 Biome (2.1.2)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx

[error] 24-24: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 25-29: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 72-72: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🔇 Additional comments (10)
apps/web/src/components/ui/select.tsx (1)

1-160: LGTM! Well-structured Radix UI Select wrapper.

The component follows React best practices with proper forwardRef usage, TypeScript typing, and displayName assignments. The className composition using the cn utility is consistent, and all Radix UI primitives are correctly wrapped and exported.

apps/web/src/types/index.ts (1)

3-3: LGTM! Consistent barrel export pattern.

The newsletter types re-export follows the established pattern and properly centralizes type access.

apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterEmptyState.tsx (1)

1-29: LGTM! Clean empty state implementation.

The component correctly handles both filtered and unfiltered empty states with appropriate messaging and user actions.

apps/web/src/components/ui/input.tsx (1)

1-22: LGTM! Well-implemented Input component.

The component follows React best practices with proper forwardRef usage, TypeScript typing, and comprehensive styling. The use of cn utility for className composition is consistent with other UI components.

apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterList.tsx (1)

12-44: LGTM! Clean implementation of grouped newsletter display.

The component properly groups newsletters by month, sorts them in descending order, and renders them in an organized structure. The use of utility functions keeps the logic clean and maintainable.

apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts (1)

1-212: LGTM! Comprehensive sample newsletter data.

The sample data is well-structured with rich content blocks covering various types (paragraphs, headings, links, lists). This provides good test coverage for the UI components.

apps/web/src/types/newsletter.ts (1)

1-42: LGTM! Well-designed type definitions.

The discriminated union for NewsletterContentItem provides type-safe content blocks, and the Newsletter interface is comprehensive with optional fields for flexibility. The types support both static and dynamic image sources appropriately.

apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterFilters.tsx (1)

24-82: LGTM!

The NewsletterFilters component is well-implemented with clean state management, responsive design, and good user experience. The active filter detection and clear filters functionality work correctly.

apps/web/src/app/(main)/dashboard/newsletters/Content.tsx (1)

16-73: LGTM!

The Newsletters component is well-structured with appropriate use of useMemo for performance optimization. The filtering logic is correctly implemented with proper state management and conditional rendering.

apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts (1)

3-26: Utility functions are well-structured.

The groupByMonth, getAvailableMonths, and formatNewsletterDate functions are correctly implemented with appropriate logic for organizing and formatting newsletter data. The only concern is with sortMonthKeys (noted separately).

Also applies to: 37-48, 51-57

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/web/next.config.js (1)

9-12: Consider adding pathname restrictions for tighter security.

The remote pattern configuration is correct and enables loading newsletter images from Unsplash. However, the pattern is broad and allows any image from images.unsplash.com.

If your newsletter images follow a predictable URL structure, consider adding a pathname field to restrict which images can be loaded.

Example with pathname restriction:

 {
   protocol: "https",
   hostname: "images.unsplash.com",
+  pathname: "/photo-*/**",
 },

Operational considerations:

  • External images introduce latency and dependency on Unsplash availability
  • Consider implementing fallback images for resilience
  • Monitor Unsplash rate limits if serving high traffic
  • Next.js will cache optimized images, but first load still depends on external service
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 34e4b80 and 43262ea.

📒 Files selected for processing (2)
  • apps/web/next.config.js (1 hunks)
  • apps/web/package.json (1 hunks)
🔇 Additional comments (1)
apps/web/package.json (1)

18-18: @radix-ui/react-select version is current and secure.

The added version 2.2.6 is the latest available, and Next.js 15 supports React 18, which matches your project configuration. No security vulnerabilities were found. The dependency addition is safe to proceed with.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts (1)

28-37: Strengthen date parsing with validation and whitespace handling.

The current implementation improves on the past review but still has gaps: it doesn't validate the split result, handle extra whitespace, or guard against invalid month names.

Consider a more robust approach using month name mapping:

+const MONTH_MAP: { [key: string]: number } = {
+  january: 0, february: 1, march: 2, april: 3, may: 4, june: 5,
+  july: 6, august: 7, september: 8, october: 9, november: 10, december: 11
+};
+
 export const sortMonthKeys = (keys: string[]): string[] => {
   return keys.sort((a, b) => {
-    // Parse month and year separately for reliable date parsing
-    const [monthA, yearA] = a.split(" ");
-    const [monthB, yearB] = b.split(" ");
-    const dateA = new Date(`${monthA} 1, ${yearA}`);
-    const dateB = new Date(`${monthB} 1, ${yearB}`);
-    return dateB.getTime() - dateA.getTime();
+    const [monthA, yearA] = a.trim().split(/\s+/);
+    const [monthB, yearB] = b.trim().split(/\s+/);
+    
+    if (!monthA || !yearA || !monthB || !yearB) {
+      return 0; // Handle malformed keys
+    }
+    
+    const monthIndexA = MONTH_MAP[monthA.toLowerCase()];
+    const monthIndexB = MONTH_MAP[monthB.toLowerCase()];
+    const numA = parseInt(yearA) * 12 + (monthIndexA ?? 0);
+    const numB = parseInt(yearB) * 12 + (monthIndexB ?? 0);
+    
+    return numB - numA;
   });
 };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 43262ea and 33f39df.

📒 Files selected for processing (3)
  • apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts (1 hunks)
  • apps/web/src/components/dashboard/Sidebar.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts (2)
apps/web/src/types/newsletter.ts (1)
  • Newsletter (33-42)
apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts (1)
  • newsletters (2-212)
🔇 Additional comments (3)
apps/web/src/components/dashboard/Sidebar.tsx (3)

20-20: LGTM: Icon import is consistent with existing patterns.

The NewspaperIcon import from @heroicons/react/24/outline correctly matches the icon library used for all other sidebar icons, resolving the previous concern about mixing icon libraries.


39-43: LGTM: Newsletters route correctly implemented and previous issues resolved.

The newsletters route addition is well-structured and consistent with existing sidebar routes. The label is correctly spelled as "Newsletters" (one word), and the icon usage follows the established pattern with className="size-5".


26-26: Import is used but appears unrelated to the newsletters feature scope—request manual verification of whether this should be in this PR.

The useFilterStore import on line 26 is actively used on lines 55 and 80 for the "Find Projects" functionality (via handleFindProjects()), not for the newsletters feature. While the import is not dead code, the original concern about scope is valid: if this is a new addition to the codebase, it should clarify whether Find Projects filtering functionality belongs in a newsletters PR or if it's a separate feature being added concurrently.

The line is marked as changed, which may indicate a new import, but the git history check failed and couldn't confirm whether this existed before.

- Implemented premium access control for newsletters, displaying a loading state and a premium gate for unpaid users.
- Added caching mechanism for newsletters data to improve performance.
- Introduced new content types for newsletters, including code snippets and tables.
- Created a dedicated component for the premium access gate with upgrade options.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx (1)

71-98: Fix inverted alignment logic.

Line 72 has a critical logic error: const isRightAligned = item.align === "left"; assigns true when alignment is "left", which is backwards. This causes lists to display with incorrect alignment.

Apply this diff to fix:

           case "list":
+            {
-            const isRightAligned = item.align === "left";
+            const isRightAligned = item.align === "right";
             
             if (isRightAligned) {
               return (
                 <ul key={index} className="my-4 space-y-2 list-none text-right">
                   {item.items.map((listItem, itemIndex) => (
                     <li
                       key={itemIndex}
                       className="text-foreground/90 flex items-center justify-end gap-2"
                     >
                       <span>{listItem}</span>
                       <span className="text-foreground/60">•</span>
                     </li>
                   ))}
                 </ul>
               );
             }
             
             return (
               <ul key={index} className="my-4 space-y-2 list-disc list-inside">
                 {item.items.map((listItem, itemIndex) => (
                   <li key={itemIndex} className="text-foreground/90">
                     {listItem}
                   </li>
                 ))}
               </ul>
             );
+            }

Also wraps the case in a block to satisfy the linter warning about switch declarations.

🧹 Nitpick comments (10)
apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts (2)

9-29: Optional: Simplify the return statement.

Line 28's || false is redundant defensive coding since matchesBasicFields is already boolean. However, it guards against matchesContent being undefined when content is empty.

If preferred, simplify to:

-  return matchesBasicFields || matchesContent || false;
+  return matchesBasicFields || (matchesContent ?? false);

37-49: Add date validation to prevent "Invalid Date" comparisons.

If newsletter.date is malformed, new Date(newsletter.date) will produce an invalid date, and toLocaleDateString will return "Invalid Date", which will never match selectedMonth but won't surface the error.

Add validation:

 const matchesMonthFilter = (
   newsletter: Newsletter,
   selectedMonth: string
 ): boolean => {
   if (selectedMonth === "all") return true;
 
   const date = new Date(newsletter.date);
+  if (isNaN(date.getTime())) {
+    console.warn(`Invalid date for newsletter ${newsletter.id}: ${newsletter.date}`);
+    return false;
+  }
   const monthYear = date.toLocaleDateString("en-US", {
     month: "long",
     year: "numeric",
   });
   return monthYear === selectedMonth;
 };
apps/web/src/app/(main)/dashboard/newsletters/page.tsx (2)

12-18: Consider enhancing the loading state.

The loading state is very basic. For a better user experience, consider using a proper loading skeleton or spinner component that matches your design system.

Example improvement:

   if (isLoading) {
     return (
-      <div className="min-h-screen bg-background flex items-center justify-center">
-        <div className="text-foreground">Loading...</div>
+      <div className="min-h-screen bg-background flex items-center justify-center">
+        <div className="flex flex-col items-center gap-4">
+          <div className="h-8 w-8 animate-spin rounded-full border-4 border-ox-purple border-t-transparent"></div>
+          <p className="text-muted-foreground">Loading newsletters...</p>
+        </div>
       </div>
     );
   }

24-28: Remove unnecessary type assertion.

Line 26 uses as Newsletter[] to cast the newsletters data, but the imported newsletters constant should already have the correct type from its definition. Type assertions can hide type mismatches.

Apply this diff:

   return (
     <div>
-      <Newsletters newsletters={newsletters as Newsletter[]} />
+      <Newsletters newsletters={newsletters} />
     </div>
   );

If TypeScript complains after removing the assertion, the root cause is likely a type mismatch that should be fixed at the source rather than suppressed with a cast.

apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx (2)

16-50: LGTM with optional optimization note.

The subscription gating and not-found handling are implemented correctly. The linear search on line 20 is acceptable for small datasets, but consider building a lookup map if the newsletter count grows significantly.

Optional: For larger datasets, pre-compute a lookup:

const newsletterMap = new Map(newsletters.map(n => [n.id, n]));
const newsletter = newsletterMap.get(id);

114-117: Remove unnecessary type assertion.

Line 116 casts newsletter.content to NewsletterContentItem[], but it should already have this type from the Newsletter interface. Type assertions can hide type mismatches.

Apply this diff:

         <div className="prose prose-lg max-w-none font-sans">
-          <NewsletterContent content={newsletter.content as NewsletterContentItem[]} />
+          <NewsletterContent content={newsletter.content} />
         </div>
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx (2)

23-37: Wrap case declarations in blocks to satisfy linter.

The linter flags that HeadingTag and headingClasses declarations can leak to other switch cases. While not a functional bug here (the case returns), wrapping in a block is best practice.

Apply this diff:

           case "heading":
+            {
             const HeadingTag = `h${item.level}` as keyof JSX.IntrinsicElements;
             const headingClasses = {
             };
             return (
               <HeadingTag
                 key={index}
                 className={headingClasses[item.level]}
               >
                 {item.content}
               </HeadingTag>
             );
+            }

60-69: Consider using Next.js Image for optimization.

The image case uses a native <img> tag. While this works, Next.js Image provides automatic optimization, lazy loading, and better performance.

However, since newsletter images use external URLs (per the data file), the current approach is acceptable. If you want to use Next.js Image, you'll need to configure remotePatterns in next.config.js.

apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts (2)

60-71: Add date validation to prevent "Invalid Date" in month filters.

Unlike groupByMonth, this function doesn't validate dates before formatting. Invalid newsletter.date values will produce "Invalid Date" strings that could appear in the month filter dropdown.

Apply this diff to add validation:

 export const getAvailableMonths = (newsletters: Newsletter[]): string[] => {
-  const months = newsletters.map((n) => {
+  const months = newsletters.flatMap((n) => {
     const date = new Date(n.date);
+    if (isNaN(date.getTime())) {
+      return []; // Skip invalid dates
+    }
     return date.toLocaleDateString("en-US", {
       month: "long",
       year: "numeric",
     });
   });
 
   const uniqueMonths = Array.from(new Set(months));
   return sortMonthKeys(uniqueMonths);
 };

Using flatMap with return [] skips invalid dates, while valid dates return an array with one element that gets flattened.


79-85: Add validation to prevent displaying "Invalid Date" to users.

This function is used to display dates in the UI. Without validation, malformed dateString values will produce "Invalid Date" that gets shown to users.

Apply this diff to add validation with a user-friendly fallback:

 export const formatNewsletterDate = (dateString: string): string => {
-  return new Date(dateString).toLocaleDateString("en-US", {
+  const date = new Date(dateString);
+  if (isNaN(date.getTime())) {
+    return "Date unavailable";
+  }
+  return date.toLocaleDateString("en-US", {
     month: "long",
     day: "numeric",
     year: "numeric",
   });
 };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 33f39df and c9dc69e.

📒 Files selected for processing (8)
  • apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterPremiumGate.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/page.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.cache.ts (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts (1 hunks)
  • apps/web/src/types/newsletter.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/types/newsletter.ts
🧰 Additional context used
🧬 Code graph analysis (7)
apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.cache.ts (2)
apps/web/src/types/newsletter.ts (1)
  • Newsletter (43-52)
apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts (1)
  • newsletters (2-212)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterPremiumGate.tsx (1)
apps/web/src/components/ui/card.tsx (4)
  • CardHeader (78-78)
  • CardTitle (80-80)
  • CardDescription (81-81)
  • CardContent (82-82)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx (1)
apps/web/src/types/newsletter.ts (1)
  • NewsletterContentItem (3-41)
apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx (6)
apps/web/src/hooks/useSubscription.ts (1)
  • useSubscription (11-75)
apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts (1)
  • newsletters (2-212)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterPremiumGate.tsx (1)
  • NewsletterPremiumGate (9-66)
apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts (1)
  • formatNewsletterDate (79-85)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx (1)
  • NewsletterContent (11-155)
apps/web/src/types/newsletter.ts (1)
  • NewsletterContentItem (3-41)
apps/web/src/app/(main)/dashboard/newsletters/page.tsx (5)
apps/web/src/hooks/useSubscription.ts (1)
  • useSubscription (11-75)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterPremiumGate.tsx (1)
  • NewsletterPremiumGate (9-66)
apps/web/src/app/(main)/dashboard/newsletters/Content.tsx (1)
  • Newsletters (16-73)
apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts (1)
  • newsletters (2-212)
apps/web/src/types/newsletter.ts (1)
  • Newsletter (43-52)
apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts (1)
apps/web/src/types/newsletter.ts (2)
  • Newsletter (43-52)
  • NewsletterContentItem (3-41)
apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts (2)
apps/web/src/types/newsletter.ts (1)
  • Newsletter (43-52)
apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts (1)
  • newsletters (2-212)
🪛 Biome (2.1.2)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx

[error] 24-24: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 25-29: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 72-72: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🔇 Additional comments (6)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterPremiumGate.tsx (1)

9-66: LGTM!

The premium gate component is well-structured with clear messaging and appropriate CTAs for both upgrading and returning to the dashboard.

apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.filters.ts (1)

59-78: LGTM!

The filtering logic correctly chains search and month filters, with appropriate handling of empty search queries.

apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx (1)

67-84: LGTM!

The conditional rendering for cover images correctly handles both string URLs and Next.js StaticImageData objects. This flexible approach supports both external URLs and imported images.

apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx (1)

46-58: LGTM!

The link rendering correctly includes target="_blank" with rel="noopener noreferrer" for security when opening external links.

apps/web/src/app/(main)/dashboard/newsletters/utils/newsletter.utils.ts (2)

8-35: LGTM!

The date validation added on lines 13-16 correctly addresses the previous review concern about handling invalid dates. Invalid newsletters are logged and skipped, preventing "Invalid Date" strings from appearing as group keys.


43-52: LGTM!

The reliable date parsing approach (lines 46-49) addresses the previous review concern by splitting month/year components and explicitly adding a day. This ensures consistent parsing across JavaScript engines.

- Replaced <img> tags with <Image> components for better optimization in newsletter pages and cards.
- Adjusted image classes to use 'object-contain' for improved layout.
- Removed the newsletter caching utility as it is no longer needed.
- Removed premium access checks and loading states from newsletters and individual newsletter pages.
- Updated NewsletterContent component to convert URLs in text to clickable links.
- Enhanced list items to support clickable links and adjusted alignment logic.
- Removed the NewsletterPremiumGate component as it is no longer needed.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx (1)

19-42: Wrap switch case variable declarations in blocks.

Multiple case clauses declare variables without block scope, which allows them to be erroneously accessed by other cases. While the current code doesn't exhibit this issue, wrapping each case body in blocks is a JavaScript/TypeScript best practice that prevents potential scope-related bugs.

Apply this diff to wrap the affected cases in blocks:

         switch (item.type) {
-          case "paragraph":
+          case "paragraph": {
             // Convert URLs in text to clickable links
             const parts = item.content.split(urlRegex);
             
             return (
               <p key={index} className="text-foreground/90 leading-relaxed">
                 {parts.map((part, partIndex) => {
                   if (part.match(urlRegex)) {
                     return (
                       <Link
                         key={partIndex}
                         href={part}
                         target="_blank"
                         rel="noopener noreferrer"
                         className="text-blue-500 hover:text-blue-600 hover:underline font-medium"
                       >
                         {part}
                       </Link>
                     );
                   }
                   return <span key={partIndex}>{part}</span>;
                 })}
               </p>
             );
+          }

-          case "heading":
+          case "heading": {
             const HeadingTag = `h${item.level}` as keyof JSX.IntrinsicElements;
             const headingClasses = {
             };
             return (
               <HeadingTag
                 key={index}
                 className={headingClasses[item.level]}
               >
                 {item.content}
               </HeadingTag>
             );
+          }

-          case "list":
+          case "list": {
             const isRightAligned = item.align === "right";
             
             const renderListItem = (listItem: string, itemIndex: number) => {
               const parts = listItem.split(urlRegex);
               return parts.map((part, partIndex) => {
                 if (part.match(urlRegex)) {
                   return (
                     <Link
                       key={partIndex}
                       href={part}
                       target="_blank"
                       rel="noopener noreferrer"
                       className="text-blue-500 hover:text-blue-600 hover:underline font-medium"
                     >
                       {part}
                     </Link>
                   );
                 }
                 return <span key={partIndex}>{part}</span>;
               });
             };
             
             if (isRightAligned) {
               return (
                 <ul key={index} className="my-4 space-y-2 list-none text-right">
                   {item.items.map((listItem, itemIndex) => (
                     <li
                       key={itemIndex}
                       className="text-foreground/90 flex items-center justify-end gap-2"
                     >
                       <span>{renderListItem(listItem, itemIndex)}</span>
                       <span className="text-foreground/60">•</span>
                     </li>
                   ))}
                 </ul>
               );
             }
             
             return (
               <ul key={index} className="my-4 space-y-2 list-disc list-inside">
                 {item.items.map((listItem, itemIndex) => (
                   <li key={itemIndex} className="text-foreground/90">
                     {renderListItem(listItem, itemIndex)}
                   </li>
                 ))}
               </ul>
             );
+          }

Also applies to: 44-58, 94-141

apps/web/src/app/(main)/dashboard/newsletters/page.tsx (3)

8-12: Consider removing the redundant wrapper div.

The outer <div> serves no purpose since the Newsletters component already provides its own wrapper.

 export default function NewslettersPage() {
-  return (
-    <div>
-      <Newsletters newsletters={newsletters as Newsletter[]} />
-    </div>
-  );
+  return <Newsletters newsletters={newsletters as Newsletter[]} />;
 }

1-1: Consider removing the "use client" directive.

This page component doesn't use any client-side hooks or browser APIs—it only passes props to the Newsletters component. In Next.js 15, server components are the default and offer better performance. The Newsletters component imported from Content.tsx (verified to have "use client" on line 1) will continue to work as a client component regardless of whether the parent page is a server or client component.

Remove the directive:

-"use client";
-
 import { Newsletter } from "@/types/newsletter";
 import Newsletters from "./Content";
 import { newsletters } from "./data/newsletters";

10-10: Add type annotation to the newsletters export to eliminate the type cast.

The data structure in newsletters.ts perfectly matches the Newsletter interface—the cast is unnecessary. Add explicit type annotation to the export in apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts:

export const newsletters: Newsletter[] = [
  // ... data
];

Then remove the cast in page.tsx:

<Newsletters newsletters={newsletters} />
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9dc69e and ee314dd.

📒 Files selected for processing (5)
  • apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/page.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterCard.tsx
  • apps/web/src/app/(main)/dashboard/newsletters/[id]/page.tsx
  • apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts
🧰 Additional context used
🧬 Code graph analysis (2)
apps/web/src/app/(main)/dashboard/newsletters/page.tsx (3)
apps/web/src/app/(main)/dashboard/newsletters/Content.tsx (1)
  • Newsletters (16-73)
apps/web/src/app/(main)/dashboard/newsletters/data/newsletters.ts (1)
  • newsletters (2-224)
apps/web/src/types/newsletter.ts (1)
  • Newsletter (43-52)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx (1)
apps/web/src/types/newsletter.ts (1)
  • NewsletterContentItem (3-41)
🪛 Biome (2.1.2)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx

[error] 21-21: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 45-45: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 46-50: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 95-95: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 97-115: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🔇 Additional comments (1)
apps/web/src/app/(main)/dashboard/newsletters/components/NewsletterContent.tsx (1)

94-141: Previous alignment logic issue has been fixed.

The inverted alignment logic flagged in the previous review has been corrected. Line 95 now properly assigns isRightAligned = item.align === "right", ensuring right-aligned lists display correctly with the bullet on the right side.

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.

[bounty-to-hire]: create ui for newsletter

1 participant