Skip to content

Conversation

@deveshru2712
Copy link
Contributor

@deveshru2712 deveshru2712 commented Nov 16, 2025

Add Newsletter Support to Opensox.in

This PR introduces a dedicated newsletter section to Opensox.in, enabling the platform to display newsletter content alongside existing blog posts.

For Non-Pro Users

  • Can only view the list of available newsletters.
  • Cannot open or read any newsletter content.
  • Restricted access ensures premium content stays exclusive.
  • Here is a video
non2.mp4

For Pro Users

  • Can fully access and read all newsletters.
  • Enjoy complete content rendering with enhanced typography.
  • Premium-only content can be introduced in the future.
  • Here is a video
pro2.mp4

Additional Enhancements

  • Added loading skeletons for a smoother and more polished user experience.

🔧 Implementation Details

📁 Content Management

  • Newsletters are stored as Markdown/MDX files under content/newsletters/.
  • Each newsletter includes frontmatter metadata:
    • title
    • summary
    • keywords
    • date
    • readTime
    • slug
  • An example file is included to define the required metadata structure.

📝 Content Processing

  • Uses gray-matter to parse frontmatter and extract metadata.
  • Markdown/MDX content is transformed into HTML with support for:
    • Headings
    • Titles
    • Images
    • Additional plugins (e.g., syntax highlighting, tables, remark/rehype plugins)

📦 Dependencies Added (Use Cases)

  • gray-matter — extracts frontmatter metadata from MD/MDX files.
  • remark — processes Markdown content.
  • remark-gfm — enables GitHub-style markdown features (tables, task lists, etc.).
  • remark-html — converts Markdown into HTML.
  • @tailwindcss/typography — improves typography styling for rendered HTML content.

I have also added sample newsletters for testing.

fixes #155

Summary by CodeRabbit

  • New Features

    • Added a Newsletter section in the dashboard with list and detail views, search, month filtering, and sort.
    • Newsletter items now show title, date, read time, summary, and keywords; skeleton loading for article view.
  • Documentation

    • Added multiple newsletter posts and an example draft with front-matter metadata.
  • Style

    • Enhanced typography styles for article content.
  • Chores

    • Added Markdown processing and typography dependencies.

@vercel
Copy link

vercel bot commented Nov 16, 2025

@deveshru2712 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 16, 2025

Walkthrough

Adds a newsletter feature: markdown content files, markdown-to-HTML processing, list and detail pages with access gating, client-side filtering UI and components, sidebar navigation entry, Tailwind typography updates, and new dependencies for markdown and typography.

Changes

Cohort / File(s) Summary
Newsletter content
apps/web/content/newsletters/example.md, apps/web/content/newsletters/test-1.md, apps/web/content/newsletters/test-2.md
Adds three newsletter Markdown files with YAML front matter (id, title, time, summary, keywords, readTime, slug) and article content (images, sections, placeholders).
Dependencies
apps/web/package.json
Adds gray-matter, remark, remark-gfm, remark-html to dependencies and @tailwindcss/typography to devDependencies.
Pages & routing
apps/web/src/app/(main)/dashboard/newsletters/page.tsx, apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx, apps/web/src/app/(main)/dashboard/newsletters/[slug]/layout.tsx
Adds newsletter listing page (server component), newsletter detail page with generateStaticParams and HTML sanitization helper, and a layout that gates access using useSubscription (redirects to /pricing for non-paid users; shows skeleton while loading).
List UI & components
apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx, apps/web/src/components/newsletter/NewsletterItem.tsx, apps/web/src/components/newsletter/Dropdown.tsx, apps/web/src/components/newsletter/NewsletterSkeleton.tsx
Implements client-side newsletter list view with search, month & sort filters, CustomDropdown (keyboard & ARIA support), newsletter card component, and a skeleton/article placeholder.
Hooks & utils
apps/web/src/hooks/useNewsletterFilters.ts, apps/web/src/utils/newsletter/newsletter-metadata.ts, apps/web/src/utils/newsletter/getNewsletterBySlug.ts
Adds useNewsletterFilters hook (search, month, sort, derived results), metadata reader (getAllNewsLetter) that parses front matter, and getNewsletterBySlug which converts Markdown to HTML via remark.
Integration & styling
apps/web/src/components/dashboard/Sidebar.tsx, apps/web/tailwind.config.ts
Adds mailbox/newsletter entry and navigation handler to sidebar (different render when collapsed/not paid) and extends Tailwind typography plugin/config with custom styles and plugin imports.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ListPage as Newsletter List Page
    participant Filters as useNewsletterFilters
    participant Item as NewsletterItem

    User->>ListPage: GET /dashboard/newsletters
    ListPage->>Filters: provide initialData
    Filters->>ListPage: filteredAndSorted
    ListPage->>User: render list + filters

    User->>ListPage: change search/month/sort
    ListPage->>Filters: update state
    Filters->>ListPage: new filtered results
    ListPage->>Item: render items
    User->>Item: click -> navigate to /newsletters/{slug}
Loading
sequenceDiagram
    participant User
    participant Layout as Newsletter Layout
    participant Auth as useSubscription
    participant Detail as Newsletter Detail Page
    participant Utils as getNewsletterBySlug
    participant Remark as remark

    User->>Layout: request /dashboard/newsletters/{slug}
    Layout->>Auth: check isLoading / isPaidUser
    alt loading
        Layout->>User: render NewsletterSkeleton
    else not paid
        Layout->>User: redirect to /pricing
    else paid
        Layout->>Detail: render detail
        Detail->>Utils: read file & front matter
        Utils->>Remark: convert markdown -> HTML
        Remark->>Utils: return HTML + metadata
        Detail->>Detail: sanitize HTML (normalize widths)
        Detail->>User: render article
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

  • Review sanitize/DOM-manipulation logic in apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx.
  • Verify file I/O and error handling in apps/web/src/utils/newsletter/getNewsletterBySlug.ts.
  • Confirm keyboard/ARIA behavior in apps/web/src/components/newsletter/Dropdown.tsx.
  • Check sidebar rendering branches in apps/web/src/components/dashboard/Sidebar.tsx for collapsed/paid states.

Possibly related PRs

Poem

🐰
gray-matter crumbs beneath my paws,
remarking stories, line by clause.
dropdowns hum and months parade,
paid doors open, skeletons fade.
I hop — the newsletter is made. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes two out-of-scope changes: the Sidebar.tsx modification adds a custom Newsletter rendering branch with OpensoxProBadge logic unrelated to the newsletter feature, and tailwind.config.ts adds extensive typography styling that extends beyond the minimal formatting requirements (bold and headings only). Remove the OpensoxProBadge branching logic from Sidebar.tsx unless explicitly required, and simplify the tailwind typography extension to only support bold text and headings as specified in issue #155.
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.
Title check ❓ Inconclusive The title 'Add: added newsletter to opensox.in' is partially related but lacks specificity. While it mentions adding newsletter functionality, it doesn't clarify the scope: it doesn't indicate this involves implementing a full newsletter page with filtering, pro-user access control, or the multiple components being added. Consider refining the title to better capture the main change, such as 'Add newsletter page with pro-user access and filtering' or 'Implement newsletter feature with pagination and month/sort filtering'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR successfully implements all core requirements from issue #155: newsletter listing with date-based organization and filtering [#155], pro-user access control with redirects [#155], rich content support for text/links/images via remark/HTML [#155], content management through Markdown files with frontmatter [#155], readability with typography styling [#155], and includes three sample newsletters, documentation, and demo videos [#155].
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (8)
apps/web/content/newsletters/example.md (1)

1-11: Template frontmatter looks solid; clarify slug usage

The structure and required fields are clear for future authors. Since getAllNewsLetter currently skips files whose name starts with example, this file won’t appear in the listing, and the slug is derived from the filename rather than this frontmatter value. Consider either aligning the slug with the filename or adding a short comment in docs that the frontmatter slug here is illustrative only.

apps/web/tailwind.config.ts (1)

2-3: Typography extension and plugins are well wired for newsletter prose

Importing tailwindcss-animate and @tailwindcss/typography, then extending theme.extend.typography.DEFAULT.css with dark-themed prose styles, is consistent and should give the newsletters a polished reading experience. The overrides for links, code, headings, blockquotes, and tables are coherent with the rest of your color system.

If you find body text too bright in practice, you could slightly reduce color from #e5e7eb to better match long-form reading comfort, but that’s purely a design tweak.

Also applies to: 125-210, 213-213

apps/web/content/newsletters/test-2.md (1)

1-68: Content and frontmatter shape are correct; consider markdownlint MD036

The frontmatter matches the newsletter loader’s expectations and the content structure reads well. If markdownlint MD036 is enforced in CI, you may want to convert the emphasized lines like **JUST START WITH WHATEVER YOU HAVE** into actual headings (e.g., ### Just start with whatever you have) or adjust the rule configuration so emphasis‑as‑heading is allowed for this style.

apps/web/src/components/newsletter/NewsletterSkeleton.tsx (1)

1-54: Skeleton layout is clean and consistent with the article view

The skeleton structure (back link, title/meta, body lines, footer) closely mirrors the final newsletter layout and should make loading feel smooth. You’re using only neutral block elements, so there’s no risk of misleading text being read.

If you want to be extra ARIA-friendly, you could wrap the skeleton content in a <div role="status" aria-live="polite" aria-busy="true">, but this is not strictly required.

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

1-8: Server → client handoff is straightforward and fits the content model

Using a server component page to synchronously read newsletters via getAllNewsLetter() and then passing them to the client NewsletterPage as initialData is clear and easy to follow.

If the number of newsletters grows or file I/O becomes noticeable, consider:

  • Switching getAllNewsLetter to async fs.promises, and/or
  • Opting into static generation (e.g., export const dynamic = "force-static" or using revalidate) depending on how often newsletter content changes.
apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx (1)

37-87: Overall listing/search/filter UX is well put together

The search input, month dropdown, sort dropdown, and “result(s) found” label provide a nice, compact filtering experience. Conditional “Clear all filters” only when filters are active is also a good touch for usability.

If you later notice frequent re-renders with larger datasets, you could debounce setSearch (e.g., via setTimeout/useDeferredValue) to keep typing snappy, but it’s not necessary for the current scale.

apps/web/src/components/dashboard/Sidebar.tsx (1)

69-71: Newsletter item doesn’t integrate with existing “active route” styling

Right now the Newsletter entry is outside SIDEBAR_ROUTES, so it never gets the getSidebarLinkClassName treatment and won’t appear active when the user is on /dashboard/newsletters (or a slug under it). This is more of a UX inconsistency than a bug.

You could align it with the other routes by wrapping the SidebarItem variant in a Link and reusing the same active‑class logic, e.g. derive a newsletterActiveClass from getSidebarLinkClassName(pathname, "/dashboard/newsletters") and apply it to a Link around the Newsletter SidebarItem (keeping the non‑paid custom block + badge as is).

Also applies to: 137-159

apps/web/src/components/newsletter/Dropdown.tsx (1)

15-26: Scope the global click listener to the open state (optional improvement)

Current behavior is correct, but the mousedown listener is attached for the entire lifetime of the component and fires on every click, even when the dropdown is closed. You can avoid unnecessary work and make the intent clearer by only registering the listener while the dropdown is open, e.g.:

-  useEffect(() => {
-    function handleClick(e: MouseEvent) {
-      if (ref.current && !ref.current.contains(e.target as Node)) {
-        setOpen(false);
-      }
-    }
-    document.addEventListener("mousedown", handleClick);
-    return () => document.removeEventListener("mousedown", handleClick);
-  }, []);
+  useEffect(() => {
+    if (!open) return;
+
+    function handleClick(e: MouseEvent) {
+      if (ref.current && !ref.current.contains(e.target as Node)) {
+        setOpen(false);
+      }
+    }
+
+    document.addEventListener("mousedown", handleClick);
+    return () => document.removeEventListener("mousedown", handleClick);
+  }, [open]);

You might also consider adding aria-haspopup="listbox" and aria-expanded={open} to the button for basic accessibility.

Also applies to: 30-48

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 97e699a and 357d163.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (16)
  • apps/web/content/newsletters/example.md (1 hunks)
  • apps/web/content/newsletters/test-1.md (1 hunks)
  • apps/web/content/newsletters/test-2.md (1 hunks)
  • apps/web/package.json (2 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/[slug]/layout.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/page.tsx (1 hunks)
  • apps/web/src/components/dashboard/Sidebar.tsx (4 hunks)
  • apps/web/src/components/newsletter/Dropdown.tsx (1 hunks)
  • apps/web/src/components/newsletter/NewsletterItem.tsx (1 hunks)
  • apps/web/src/components/newsletter/NewsletterSkeleton.tsx (1 hunks)
  • apps/web/src/hooks/useNewsletterFilters.ts (1 hunks)
  • apps/web/src/utils/newsletter/getAllNewsLetter.ts (1 hunks)
  • apps/web/src/utils/newsletter/getNewsletterBySlug.ts (1 hunks)
  • apps/web/tailwind.config.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx (3)
apps/web/src/hooks/useNewsletterFilters.ts (1)
  • useNewsletterFilters (13-92)
apps/web/src/components/newsletter/Dropdown.tsx (1)
  • CustomDropdown (6-77)
apps/web/src/components/newsletter/NewsletterItem.tsx (1)
  • NewsletterItem (13-85)
apps/web/src/hooks/useNewsletterFilters.ts (1)
apps/web/src/components/newsletter/NewsletterItem.tsx (1)
  • NewsletterItem (13-85)
apps/web/src/app/(main)/dashboard/newsletters/page.tsx (2)
apps/web/src/utils/newsletter/getAllNewsLetter.ts (1)
  • getAllNewsLetter (17-50)
apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx (1)
  • NewsletterPage (18-124)
apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx (1)
apps/web/src/utils/newsletter/getNewsletterBySlug.ts (2)
  • getAllNewsletterSlugs (10-15)
  • getNewsletterBySlug (17-32)
apps/web/src/app/(main)/dashboard/newsletters/[slug]/layout.tsx (2)
apps/web/src/hooks/useSubscription.ts (1)
  • useSubscription (11-77)
apps/web/src/components/newsletter/NewsletterSkeleton.tsx (1)
  • NewsletterSkeleton (1-54)
apps/web/src/components/dashboard/Sidebar.tsx (2)
apps/web/src/components/sheet/OpensoxProBadge.tsx (1)
  • OpensoxProBadge (8-28)
apps/web/src/components/sidebar/SidebarItem.tsx (1)
  • SidebarItem (13-32)
🪛 ast-grep (0.39.9)
apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx

[warning] 91-91: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)
apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx

[error] 92-92: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🪛 LanguageTool
apps/web/content/newsletters/test-2.md

[style] ~30-~30: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...till publish it and let people use it. When your resume looks like a blank slate, b...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

apps/web/content/newsletters/test-1.md

[style] ~28-~28: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...ing. No college degree. No connections. No safety net. Just documentation, tutoria...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~61-~61: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...mission. You don't need a fancy degree. You need a computer, internet, and the refu...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🪛 markdownlint-cli2 (0.18.1)
apps/web/content/newsletters/test-2.md

58-58: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


60-60: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


62-62: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

🔇 Additional comments (5)
apps/web/content/newsletters/test-1.md (1)

1-63: Frontmatter and structure align with the newsletter loader

This newsletter’s frontmatter matches the expected fields (id, title, date, summary, keywords, readTime, slug) and should be correctly picked up by getAllNewsLetter. The narrative style (repeated “No …” / “You …” sentence starts) is intentional and works rhetorically; LanguageTool’s suggestions there can be safely ignored unless you want stricter formal tone.

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

95-121: Use useRouter().push() for client-side navigation instead of redirect

In Next.js 15, redirect from "next/navigation" is intended for Server Components, Server Actions, and route handlers, not for event handlers in client components. For client-side onClick handlers, use the client-only useRouter() with router.push() or router.replace().

Update NewsletterItem to use client-side routing:

+"use client";
+
+import { useRouter } from "next/navigation";
+
 export default function NewsletterItem({
   time,
   title,
   summary,
   keywords,
   readTime,
   slug,
 }: NewsletterItemProps) {
+  const router = useRouter();
+
   return (
     <div
       className="
         w-full cursor-pointer group
         border border-white/10
         rounded-lg
         p-4
         hover:border-purple-400/40
         transition-all duration-200
       "
-      onClick={() => redirect(`/dashboard/newsletters/${slug}`)}
+      onClick={() => router.push(`/dashboard/newsletters/${slug}`)}
     >
       {/* ... */}
     </div>
   );
 }

Alternatively, wrap the content in a <Link href={/dashboard/newsletters/${slug}}> for simpler markup.

Likely an incorrect or invalid review comment.

apps/web/package.json (1)

30-42: Compatibility confirmed; versions are safe to use with sensible sanitization practices

Gray-matter@4, remark@15, remark-gfm@4, and remark-html@16 are generally compatible with Next.js 15 (React 18/19), provided your Node version meets Next.js 15 requirements. remark-html is safe by default (it uses a safe schema).

For untrusted content, follow standard practice: sanitize HTML server-side during build/SSR and insert client-side via dangerouslySetInnerHTML. rehype-sanitize or DOMPurify (which you already depend on) are common choices. No action needed if you're already following this pattern.

apps/web/src/app/(main)/dashboard/newsletters/[slug]/layout.tsx (1)

8-30: Access gating and loading flow look correct for pro‑only detail view

The layout cleanly gates newsletter detail pages behind isPaidUser and shows the skeleton while subscription status is resolving; redirecting non‑paid users to /pricing once loading completes matches the intended behavior and avoids flashing protected content.

apps/web/src/hooks/useNewsletterFilters.ts (1)

44-69: Filtering pipeline looks solid.

Nicely composed pipeline—search, month filter, and sort are all handled cleanly and stay memoized for cheap updates.

Comment on lines +10 to +15
export function getAllNewsletterSlugs(): string[] {
return fs
.readdirSync(newsletterDir)
.filter((f) => f.endsWith(".md"))
.map((f) => f.replace(/\.md$/, ""));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Include .mdx files when building slug list.

Right now the slug generator ignores every .mdx file, so those newsletters never get static params or routes even though the rest of the stack treats MDX as supported content. Update the filter/replace so both .md and .mdx files participate.

Apply this diff:

-    .readdirSync(newsletterDir)
-    .filter((f) => f.endsWith(".md"))
-    .map((f) => f.replace(/\.md$/, ""));
+    .readdirSync(newsletterDir)
+    .filter((f) => f.endsWith(".md") || f.endsWith(".mdx"))
+    .map((f) => f.replace(/\.mdx?$/, ""));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function getAllNewsletterSlugs(): string[] {
return fs
.readdirSync(newsletterDir)
.filter((f) => f.endsWith(".md"))
.map((f) => f.replace(/\.md$/, ""));
}
export function getAllNewsletterSlugs(): string[] {
return fs
.readdirSync(newsletterDir)
.filter((f) => f.endsWith(".md") || f.endsWith(".mdx"))
.map((f) => f.replace(/\.mdx?$/, ""));
}
🤖 Prompt for AI Agents
In apps/web/src/utils/newsletter/getNewsletterBySlug.ts around lines 10 to 15,
the slug generator only includes files ending with ".md" so ".mdx" newsletters
are ignored; update the file filter to accept both ".md" and ".mdx" (e.g., test
the extension with a regex like /\.(md|mdx)$/) and update the mapping that
strips the extension to remove either ".md" or ".mdx" (e.g., replace using
/\.mdx?$/) so both file types produce slugs.

Comment on lines +21 to +30
const fullPath = path.join(newsletterDir, `${slug}.md`);
const file = fs.readFileSync(fullPath, "utf8");

const { data, content } = matter(file);

const processed = await remark().use(gfm).use(html).process(content);

return {
metadata: data,
html: processed.toString(),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Resolve .mdx file loads in getNewsletterBySlug.

We only try ${slug}.md, so any .mdx newsletter will throw ENOENT even though we advertise MDX support. Probe both extensions (or detect the actual file) before reading.

Apply this diff:

-  const fullPath = path.join(newsletterDir, `${slug}.md`);
-  const file = fs.readFileSync(fullPath, "utf8");
+  const candidates = [
+    path.join(newsletterDir, `${slug}.md`),
+    path.join(newsletterDir, `${slug}.mdx`),
+  ];
+  const fullPath = candidates.find((candidate) => fs.existsSync(candidate));
+  if (!fullPath) {
+    throw new Error(`Newsletter "${slug}" not found as .md or .mdx`);
+  }
+  const file = fs.readFileSync(fullPath, "utf8");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fullPath = path.join(newsletterDir, `${slug}.md`);
const file = fs.readFileSync(fullPath, "utf8");
const { data, content } = matter(file);
const processed = await remark().use(gfm).use(html).process(content);
return {
metadata: data,
html: processed.toString(),
const candidates = [
path.join(newsletterDir, `${slug}.md`),
path.join(newsletterDir, `${slug}.mdx`),
];
const fullPath = candidates.find((candidate) => fs.existsSync(candidate));
if (!fullPath) {
throw new Error(`Newsletter "${slug}" not found as .md or .mdx`);
}
const file = fs.readFileSync(fullPath, "utf8");
const { data, content } = matter(file);
const processed = await remark().use(gfm).use(html).process(content);
return {
metadata: data,
html: processed.toString(),
🤖 Prompt for AI Agents
In apps/web/src/utils/newsletter/getNewsletterBySlug.ts around lines 21 to 30,
the function only tries `${slug}.md` and will throw ENOENT for `.mdx` files;
update it to check for both `.md` and `.mdx` (e.g., test existence with
fs.existsSync or fs.promises.stat for `${slug}.md` then `${slug}.mdx`) and pick
the first existing path before reading; if neither exists, throw a clear error;
then read and parse the chosen file path as before so MDX newsletters are
supported.

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

🧹 Nitpick comments (8)
apps/web/tailwind.config.ts (1)

126-211: Consider using theme color references for maintainability.

The typography configuration uses several hardcoded color values that duplicate colors already defined in your theme:

  • #9455f4 (line 132) matches ox-purple
  • #7A45C3 (line 134) matches ox-purple-2
  • #252525 (line 188, 207) matches border-primary

To improve maintainability and ensure color consistency, consider referencing theme colors or CSS variables instead of hardcoding values. Example approach:

 typography: {
   DEFAULT: {
     css: {
       maxWidth: "none",
       color: "#e5e7eb",
       a: {
-        color: "#9455f4",
+        color: "var(--ox-purple)",
         "&:hover": {
-          color: "#7A45C3",
+          color: "var(--ox-purple-2)",
         },
         textDecoration: "none",
       },
       // ... rest of config
       hr: {
-        borderColor: "#252525",
+        borderColor: "var(--border-primary)",
       },

Note: You'll need to ensure these CSS variables are defined in your global styles, or use Tailwind's theme color references if the typography plugin supports it.

apps/web/content/newsletters/test-1.md (1)

18-18: Consider hosting images locally instead of hotlinking.

The newsletter uses an external image URL from Brave search results (imgs.search.brave.com). Hotlinking external images has several drawbacks:

  • Image availability depends on external service
  • No control over image content changes
  • Potential performance and privacy implications
  • The URL may break or change over time

Consider:

  1. Download and host the image in your public assets directory
  2. Use a proper CDN or image hosting service
  3. Add the image to a public/newsletters/ directory and reference it with a relative path

Example:

-![Journey from farm to founder](https://imgs.search.brave.com/0NvT2qcazrFJeTKxSWwaaSCfq2Ak2DCGgdHPup131xQ/rs:fit:860:0:0:0/g:ce/aHR0cHM6Ly93YWxs/cGFwZXJzLmNvbS9p/bWFnZXMvZmVhdHVy/ZWQvaGFyZC13b3Jr/LXBpY3R1cmVzLTMw/YjY2amNnc203aGw2/djIuanBn)
+![Journey from farm to founder](/newsletters/farm-to-founder.jpg)
apps/web/src/app/(main)/dashboard/newsletters/[slug]/layout.tsx (2)

16-20: Potential flash of content before redirect.

The redirect logic has a timing issue: the component renders null (line 27) while the useEffect redirect is still pending. During this brief moment, users might see a blank screen before the redirect completes. Additionally, using router.replace might cause browser history issues if users try to navigate back.

Consider showing a more informative message or using router.push for better UX:

 useEffect(() => {
   if (!isLoading && !isPaidUser) {
-    router.replace("/pricing");
+    router.push("/pricing");
   }
 }, [isLoading, isPaidUser, router]);

 if (isLoading) {
   return <NewsletterSkeleton />;
 }

 if (!isPaidUser) {
-  return null;
+  return (
+    <div className="flex items-center justify-center min-h-screen">
+      <p className="text-gray-400">Redirecting to pricing...</p>
+    </div>
+  );
 }

Alternatively, consider using Next.js middleware for authentication checks to avoid client-side redirects altogether.


8-31: Consider server-side authentication check.

This layout performs access control entirely on the client side, which means:

  1. The newsletter content might be fetched before the redirect occurs
  2. Users can potentially access data by disabling JavaScript or intercepting requests
  3. Additional network round trip for subscription check

For sensitive content like paid newsletters, consider:

  • Adding server-side authentication in the page component or using Next.js middleware
  • Checking subscription status before serving the page content
  • Using server-side redirects for better security and UX

This would prevent unauthorized access attempts and improve loading performance by avoiding unnecessary data fetching.

apps/web/content/newsletters/test-2.md (1)

27-30: Minor: Consider varying sentence structure for better flow.

Three consecutive sentences starting with "When" may impact readability. Consider rephrasing one for variety.

Example:

-When your camera sucks, but still you hit that red button, record a scrappy and embarrassing video, and click on "post" and actually post it on YouTube.
+Your camera sucks, but you still hit that red button, record a scrappy and embarrassing video, and click on "post" and actually post it on YouTube.
apps/web/src/utils/newsletter/getAllNewsLetter.ts (1)

17-50: Consider adding error handling for file parsing.

The function uses synchronous file operations without error handling. Malformed markdown or frontmatter could cause build failures.

Add try-catch around file operations:

 export function getAllNewsLetter(): Newsletter[] {
   const files = fs.readdirSync(newsletterDir);
 
   return files
     .filter((file) => {
       // Skip files starting with "example"
       if (file.startsWith("example")) return false;
       // Only include .md and .mdx files
       return file.endsWith(".md") || file.endsWith(".mdx");
     })
     .map((filename) => {
       const filePath = path.join(newsletterDir, filename);
-      const raw = fs.readFileSync(filePath, "utf8");
-      const { data } = matter(raw);
+      try {
+        const raw = fs.readFileSync(filePath, "utf8");
+        const { data } = matter(raw);
 
-      const slug = filename.replace(/\.mdx?$/, "");
+        const slug = filename.replace(/\.mdx?$/, "");
 
-      const keywords = Array.isArray(data.keywords)
-        ? data.keywords
-        : typeof data.keywords === "string"
-          ? data.keywords.split(",").map((k: string) => k.trim())
-          : [];
+        const keywords = Array.isArray(data.keywords)
+          ? data.keywords
+          : typeof data.keywords === "string"
+            ? data.keywords.split(",").map((k: string) => k.trim())
+            : [];
 
-      return {
-        id: data.id ?? slug,
-        title: data.title ?? "Untitled Newsletter",
-        summary: data.summary ?? "",
-        keywords,
-        time: data.time ?? data.date ?? new Date().toISOString(),
-        readTime: data.readTime ?? "1 min read",
-        slug,
-      } satisfies Newsletter;
+        return {
+          id: data.id ?? slug,
+          title: data.title ?? "Untitled Newsletter",
+          summary: data.summary ?? "",
+          keywords,
+          time: data.time ?? data.date ?? new Date().toISOString(),
+          readTime: data.readTime ?? "1 min read",
+          slug,
+        } satisfies Newsletter;
+      } catch (error) {
+        console.error(`Failed to parse newsletter: ${filename}`, error);
+        throw new Error(`Invalid newsletter file: ${filename}`);
+      }
     });
 }
apps/web/src/components/newsletter/Dropdown.tsx (1)

55-55: Consider lowering z-index value.

z-[9999] is an extremely high z-index that might cause stacking conflicts with modals or other overlays.

Consider using a more reasonable value like z-50 unless there's a specific need for such a high value.

           className="
             absolute mt-2 w-full
             bg-neutral-900 text-white
             border border-white/10 
             rounded-md shadow-lg shadow-black/40
-            z-[9999]
+            z-50
           "
apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx (1)

8-16: Duplicate type definition: reuse Newsletter from utility.

The NewsletterData interface duplicates the Newsletter type from getAllNewsLetter.ts, creating maintenance overhead.

+import { Newsletter } from "@/utils/newsletter/getAllNewsLetter";
 import { Search } from "lucide-react";
 import NewsletterItem from "@/components/newsletter/NewsletterItem";
 import CustomDropdown from "@/components/newsletter/Dropdown";
 import { useNewsletterFilters } from "@/hooks/useNewsletterFilters";
 
-interface NewsletterData {
-  id: string;
-  title: string;
-  summary: string;
-  keywords: string[];
-  time: string;
-  readTime: string;
-  slug: string;
-}
-
 export default function NewsletterPage({
   initialData,
 }: {
-  initialData: NewsletterData[];
+  initialData: Newsletter[];
 }) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 97e699a and 61228ce.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (16)
  • apps/web/content/newsletters/example.md (1 hunks)
  • apps/web/content/newsletters/test-1.md (1 hunks)
  • apps/web/content/newsletters/test-2.md (1 hunks)
  • apps/web/package.json (2 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/[slug]/layout.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/page.tsx (1 hunks)
  • apps/web/src/components/dashboard/Sidebar.tsx (5 hunks)
  • apps/web/src/components/newsletter/Dropdown.tsx (1 hunks)
  • apps/web/src/components/newsletter/NewsletterItem.tsx (1 hunks)
  • apps/web/src/components/newsletter/NewsletterSkeleton.tsx (1 hunks)
  • apps/web/src/hooks/useNewsletterFilters.ts (1 hunks)
  • apps/web/src/utils/newsletter/getAllNewsLetter.ts (1 hunks)
  • apps/web/src/utils/newsletter/getNewsletterBySlug.ts (1 hunks)
  • apps/web/tailwind.config.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
apps/web/src/hooks/useNewsletterFilters.ts (1)
apps/web/src/components/newsletter/NewsletterItem.tsx (1)
  • NewsletterItem (13-85)
apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx (3)
apps/web/src/hooks/useNewsletterFilters.ts (1)
  • useNewsletterFilters (13-92)
apps/web/src/components/newsletter/Dropdown.tsx (1)
  • CustomDropdown (6-77)
apps/web/src/components/newsletter/NewsletterItem.tsx (1)
  • NewsletterItem (13-85)
apps/web/src/app/(main)/dashboard/newsletters/[slug]/layout.tsx (2)
apps/web/src/hooks/useSubscription.ts (1)
  • useSubscription (11-77)
apps/web/src/components/newsletter/NewsletterSkeleton.tsx (1)
  • NewsletterSkeleton (1-54)
apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx (1)
apps/web/src/utils/newsletter/getNewsletterBySlug.ts (2)
  • getAllNewsletterSlugs (10-15)
  • getNewsletterBySlug (17-32)
apps/web/src/app/(main)/dashboard/newsletters/page.tsx (2)
apps/web/src/utils/newsletter/getAllNewsLetter.ts (1)
  • getAllNewsLetter (17-50)
apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx (1)
  • NewsletterPage (18-124)
apps/web/src/components/dashboard/Sidebar.tsx (2)
apps/web/src/components/sheet/OpensoxProBadge.tsx (1)
  • OpensoxProBadge (8-28)
apps/web/src/components/sidebar/SidebarItem.tsx (1)
  • SidebarItem (13-36)
🪛 ast-grep (0.39.9)
apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx

[warning] 91-91: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)
apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx

[error] 92-92: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🪛 LanguageTool
apps/web/content/newsletters/test-1.md

[style] ~28-~28: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...ing. No college degree. No connections. No safety net. Just documentation, tutoria...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~61-~61: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...mission. You don't need a fancy degree. You need a computer, internet, and the refu...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

apps/web/content/newsletters/test-2.md

[style] ~30-~30: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...till publish it and let people use it. When your resume looks like a blank slate, b...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🪛 markdownlint-cli2 (0.18.1)
apps/web/content/newsletters/test-2.md

58-58: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


60-60: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


62-62: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

🔇 Additional comments (10)
apps/web/tailwind.config.ts (2)

2-3: LGTM: Modern ES module imports.

The conversion from require() to ES module imports is the correct approach for TypeScript configuration files.


214-214: LGTM: Plugin configuration updated correctly.

The plugins array correctly uses the imported plugin references, consistent with the ES module conversion.

apps/web/src/hooks/useNewsletterFilters.ts (1)

44-69: LGTM: Filter and sort logic is well-structured.

The filtering and sorting implementation is clean and efficient:

  • Uses useMemo appropriately to avoid unnecessary recalculations
  • Case-insensitive search across relevant fields
  • Clear sort logic with proper date comparison
apps/web/src/components/newsletter/NewsletterSkeleton.tsx (1)

1-54: LGTM: Clean skeleton implementation.

The skeleton component is well-structured with:

  • Appropriate use of animate-pulse for loading feedback
  • Responsive layout matching the actual content structure
  • Consistent styling with the design system
apps/web/content/newsletters/test-1.md (1)

1-16: LGTM: Front matter structure is correct.

The front matter follows the expected schema with all required fields properly defined.

apps/web/content/newsletters/example.md (1)

1-11: LGTM: Example file serves its purpose.

This file serves as a template/documentation for the newsletter front matter structure. According to the getAllNewsLetter utility, files starting with "example" are intentionally filtered out from the newsletter list, so this won't appear in production.

The front matter structure correctly demonstrates all required fields for new newsletters.

apps/web/package.json (1)

30-30: All package versions are secure with no active vulnerabilities.

The security scan confirms that the specified versions are safe:

  • gray-matter ^4.0.3: No vulnerabilities
  • remark ^18.0.0: No vulnerabilities
  • remark-gfm ^4.0.1: No vulnerabilities
  • remark-html ^16.0.1: No vulnerabilities (version ^16.0.1 is outside known vulnerable ranges for older versions ≤14.0.0 and <13.0.2)
  • @tailwindcss/typography ^0.5.15: No vulnerabilities
apps/web/content/newsletters/test-2.md (1)

58-64: Bold emphasis for impact is appropriate here.

While markdown linters flag these lines as "emphasis used instead of heading," the bold styling is intentionally used for emphasis and impact rather than document structure. This is a valid stylistic choice for motivational content.

apps/web/src/components/dashboard/Sidebar.tsx (1)

136-158: Verify hover color consistency.

The Newsletter item uses hover:bg-[#121214] (line 138) while other items use hover:bg-[#292929] (line 162). Ensure this difference is intentional.

If consistency is desired, apply this diff:

         {!isCollapsed && !isPaidUser ? (
           <div
-            className="w-full h-[44px] flex items-center rounded-md cursor-pointer transition-colors px-2 gap-3 pl-3 hover:bg-[#121214]"
+            className="w-full h-[44px] flex items-center rounded-md cursor-pointer transition-colors px-2 gap-3 pl-3 hover:bg-[#292929] group"
             onClick={handleNewsLetter}
           >
             <span className="shrink-0 text-[#eaeaea]">
               <Mailbox className="size-5" />
             </span>
             <div className="flex items-center gap-1">
               <h1 className="text-xs font-medium text-[#c8c8c8] group-hover:text-ox-purple">
                 Newsletter
               </h1>
apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx (1)

87-93: Review comment is incorrect - no XSS vulnerability exists with current implementation.

The review references a non-existent function ("removeWidthStyles") when the actual function is "sanitizeHtml". More importantly, the actual XSS protection comes from the markdown processing pipeline, not from the width-removal function. Newsletters are processed through remark-html, which escapes HTML by default, ensuring that any HTML in the markdown content is safe. Since newsletters are sourced from markdown files only (not user input), and remark-html escapes the output, there is no XSS vulnerability. The sanitizeHtml function only removes width attributes via regex, which is orthogonal to XSS protection but works correctly for its stated purpose.

The recommendation to add DOMPurify is unnecessary given the safe markdown-to-HTML pipeline already in place.

Likely an incorrect or invalid review comment.

Comment on lines +4 to +8
export default function Page() {
const newsletters = getAllNewsLetter();

return <NewsletterPage initialData={newsletters} />;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add error handling for newsletter loading failures.

The getAllNewsLetter() call has no error handling. If the function throws (e.g., due to file system errors, malformed content, or missing directory), the entire page will crash.

Consider adding error boundaries or try-catch:

 export default function Page() {
-  const newsletters = getAllNewsLetter();
+  try {
+    const newsletters = getAllNewsLetter();
+    return <NewsletterPage initialData={newsletters} />;
+  } catch (error) {
+    console.error("Failed to load newsletters:", error);
+    return (
+      <div className="w-full py-6 px-4 sm:px-8">
+        <h1 className="text-3xl font-bold">Newsletter</h1>
+        <p className="mt-4 text-red-400">
+          Failed to load newsletters. Please try again later.
+        </p>
+      </div>
+    );
+  }
-
-  return <NewsletterPage initialData={newsletters} />;
 }
🤖 Prompt for AI Agents
In apps/web/src/app/(main)/dashboard/newsletters/page.tsx around lines 4 to 8,
the call to getAllNewsLetter() is unprotected and will crash the page if it
throws; wrap the call in a try-catch, log the error (with contextual message),
and return a safe fallback render—e.g., render NewsletterPage with an empty
initialData array and/or an error prop informing the UI—or rethrow a controlled
error to trigger a Next.js error boundary (or call notFound/redirect) depending
on desired UX; ensure the catch path does not expose sensitive details and
provides enough info for debugging.

Comment on lines 18 to 31
const availableMonths = useMemo(() => {
const months = new Set<string>();
initialData.forEach((item) => {
const date = new Date(item.time);
const monthYear = date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
});
months.add(monthYear);
});
return Array.from(months).sort(
(a, b) => new Date(b).getTime() - new Date(a).getTime()
);
}, [initialData]);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add error handling for invalid date values.

The date parsing logic assumes item.time is always a valid date string. If an invalid value is encountered, new Date(item.time) returns an Invalid Date object, which could cause unexpected behavior.

Consider adding validation:

 const availableMonths = useMemo(() => {
   const months = new Set<string>();
   initialData.forEach((item) => {
     const date = new Date(item.time);
+    // Skip invalid dates
+    if (isNaN(date.getTime())) {
+      console.warn(`Invalid date in newsletter: ${item.id || item.slug}`);
+      return;
+    }
     const monthYear = date.toLocaleDateString("en-US", {
       year: "numeric",
       month: "long",
     });
     months.add(monthYear);
   });

Also apply similar validation in the filteredByMonth filter (lines 53-61).

🤖 Prompt for AI Agents
In apps/web/src/hooks/useNewsletterFilters.ts around lines 18 to 31, the
month-extraction logic assumes item.time is always valid and uses new
Date(item.time) without checking for Invalid Date; update the code to validate
the parsed date (e.g., create the Date, check !isNaN(date.getTime()) or
date.toString() !== "Invalid Date") before computing monthYear and only add
valid monthYear values to the Set, and ensure you handle or skip invalid
entries; also apply the same validation pattern in the filteredByMonth logic
around lines 53 to 61 so the filter skips items with invalid dates instead of
producing incorrect matches or runtime errors.

Comment on lines +10 to +15
export function getAllNewsletterSlugs(): string[] {
return fs
.readdirSync(newsletterDir)
.filter((f) => f.endsWith(".md"))
.map((f) => f.replace(/\.md$/, ""));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Inconsistency: Handle both .md and .mdx files.

This function only processes .md files, but getAllNewsLetter.ts supports both .md and .mdx. This inconsistency could cause issues if .mdx files are added.

 export function getAllNewsletterSlugs(): string[] {
   return fs
     .readdirSync(newsletterDir)
-    .filter((f) => f.endsWith(".md"))
-    .map((f) => f.replace(/\.md$/, ""));
+    .filter((f) => f.endsWith(".md") || f.endsWith(".mdx"))
+    .map((f) => f.replace(/\.mdx?$/, ""));
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function getAllNewsletterSlugs(): string[] {
return fs
.readdirSync(newsletterDir)
.filter((f) => f.endsWith(".md"))
.map((f) => f.replace(/\.md$/, ""));
}
export function getAllNewsletterSlugs(): string[] {
return fs
.readdirSync(newsletterDir)
.filter((f) => f.endsWith(".md") || f.endsWith(".mdx"))
.map((f) => f.replace(/\.mdx?$/, ""));
}
🤖 Prompt for AI Agents
In apps/web/src/utils/newsletter/getNewsletterBySlug.ts around lines 10 to 15,
the function only filters for ".md" files while other code accepts both ".md"
and ".mdx"; update the file listing logic to include both extensions (e.g.,
filter for filenames that end with ".md" or ".mdx"), strip either extension when
mapping to slugs (remove the matching ".md" or ".mdx" suffix), and optionally
deduplicate the resulting slug array in case both versions exist for the same
slug.

Comment on lines 17 to 32
export async function getNewsletterBySlug(slug: string): Promise<{
metadata: Record<string, any>;
html: string;
}> {
const fullPath = path.join(newsletterDir, `${slug}.md`);
const file = fs.readFileSync(fullPath, "utf8");

const { data, content } = matter(file);

const processed = await remark().use(gfm).use(html).process(content);

return {
metadata: data,
html: processed.toString(),
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling and support .mdx files.

The function lacks error handling and only supports .md files, creating inconsistency with getAllNewsLetter.ts.

 export async function getNewsletterBySlug(slug: string): Promise<{
   metadata: Record<string, any>;
   html: string;
 }> {
-  const fullPath = path.join(newsletterDir, `${slug}.md`);
-  const file = fs.readFileSync(fullPath, "utf8");
+  // Try .md first, then .mdx
+  let fullPath = path.join(newsletterDir, `${slug}.md`);
+  if (!fs.existsSync(fullPath)) {
+    fullPath = path.join(newsletterDir, `${slug}.mdx`);
+  }
+  
+  if (!fs.existsSync(fullPath)) {
+    throw new Error(`Newsletter not found: ${slug}`);
+  }
+  
+  const file = fs.readFileSync(fullPath, "utf8");
 
   const { data, content } = matter(file);
 
   const processed = await remark().use(gfm).use(html).process(content);
 
   return {
     metadata: data,
     html: processed.toString(),
   };
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function getNewsletterBySlug(slug: string): Promise<{
metadata: Record<string, any>;
html: string;
}> {
const fullPath = path.join(newsletterDir, `${slug}.md`);
const file = fs.readFileSync(fullPath, "utf8");
const { data, content } = matter(file);
const processed = await remark().use(gfm).use(html).process(content);
return {
metadata: data,
html: processed.toString(),
};
}
export async function getNewsletterBySlug(slug: string): Promise<{
metadata: Record<string, any>;
html: string;
}> {
// Try .md first, then .mdx
let fullPath = path.join(newsletterDir, `${slug}.md`);
if (!fs.existsSync(fullPath)) {
fullPath = path.join(newsletterDir, `${slug}.mdx`);
}
if (!fs.existsSync(fullPath)) {
throw new Error(`Newsletter not found: ${slug}`);
}
const file = fs.readFileSync(fullPath, "utf8");
const { data, content } = matter(file);
const processed = await remark().use(gfm).use(html).process(content);
return {
metadata: data,
html: processed.toString(),
};
}
🤖 Prompt for AI Agents
In apps/web/src/utils/newsletter/getNewsletterBySlug.ts around lines 17 to 32,
the function currently reads only `.md` files and has no error handling; update
it to attempt reading `${slug}.md` and if not found fall back to `${slug}.mdx`,
wrap the file read / parsing / remark processing in a try/catch, and in the
catch log or rethrow a clear error (or return null) so callers can handle
failures; ensure the processor supports MDX if needed (or detect .mdx and use
the proper parser), and return the same shape { metadata, html } on success.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx (2)

14-20: Good rename from past review; consider adding documentation.

The rename from sanitizeHtml to normalizeContentWidths addresses the previous review feedback well—the name now accurately reflects that this function handles layout normalization, not security.

To help future maintainers, consider adding a brief JSDoc comment documenting:

  • Its purpose (removes width-related attributes for responsive layout)
  • What it doesn't do (XSS protection)
  • The trust assumption (content sourced from local markdown files in the repo)

Example:

+/**
+ * Removes width-related inline styles and attributes for responsive layout.
+ * Note: This is NOT security sanitization. Content is assumed trusted
+ * (sourced from local markdown files maintained by the team).
+ */
 function normalizeContentWidths(html: string): string {

86-93: Consider adding HTML sanitization for defense-in-depth.

While the current content source is trusted (local markdown files), adding proper HTML sanitization would provide defense-in-depth protection and make the codebase more resilient to future changes.

Since DOMPurify is already available in the project dependencies, you could integrate it in the content processing pipeline to sanitize the HTML before rendering. This would protect against XSS if the content source ever changes (e.g., future CMS integration, external API).

Example integration in getNewsletterBySlug.ts or before rendering:

import DOMPurify from 'isomorphic-dompurify';

// In component or utility:
const sanitizedHtml = DOMPurify.sanitize(normalizedHtml, {
  ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'img', 'pre', 'code', 'blockquote', 'table', 'thead', 'tbody', 'tr', 'th', 'td'],
  ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class']
});
apps/web/src/components/newsletter/Dropdown.tsx (1)

70-75: Optionally focus the selected option instead of always the first.

Right now opening the dropdown always focuses the first option; UX is often better if focus lands on the currently selected value, especially for keyboard users.

Example refactor:

-  useEffect(() => {
-    if (open) {
-      optionsRef.current[0]?.focus();
-    }
-  }, [open]);
+  useEffect(() => {
+    if (!open) return;
+
+    const selectedIndex = options.findIndex((opt) => opt.value === value);
+    const indexToFocus = selectedIndex >= 0 ? selectedIndex : 0;
+    optionsRef.current[indexToFocus]?.focus();
+  }, [open, options, value]);

This keeps behavior unchanged when no matching value is found, but improves navigation when reopening an already‑selected dropdown.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 61228ce and 30f4fbd.

📒 Files selected for processing (7)
  • apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx (1 hunks)
  • apps/web/src/components/newsletter/Dropdown.tsx (1 hunks)
  • apps/web/src/components/newsletter/NewsletterItem.tsx (1 hunks)
  • apps/web/src/hooks/useNewsletterFilters.ts (1 hunks)
  • apps/web/src/utils/newsletter/getAllNewsLetter.ts (1 hunks)
  • apps/web/src/utils/newsletter/getNewsletterBySlug.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/web/src/utils/newsletter/getNewsletterBySlug.ts
  • apps/web/src/utils/newsletter/getAllNewsLetter.ts
  • apps/web/src/components/newsletter/NewsletterItem.tsx
  • apps/web/src/hooks/useNewsletterFilters.ts
  • apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx (1)
apps/web/src/utils/newsletter/getNewsletterBySlug.ts (2)
  • getAllNewsletterSlugs (10-15)
  • getNewsletterBySlug (17-32)
🪛 ast-grep (0.39.9)
apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx

[warning] 91-91: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)
apps/web/src/components/newsletter/Dropdown.tsx

[error] 42-42: 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] 47-48: 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)

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

[error] 92-92: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🔇 Additional comments (5)
apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx (4)

1-8: LGTM!

Imports are clean and appropriate for the newsletter detail page functionality.


10-12: LGTM!

Static params generation follows Next.js conventions correctly and will enable pre-rendering of all newsletter detail pages.


22-34: LGTM!

Component setup and error handling follow Next.js server component patterns correctly. The try-catch with notFound() ensures missing or invalid newsletters result in proper 404 responses.


38-110: LGTM!

The UI implementation is well-structured with:

  • Semantic HTML elements (article, header, section, footer, time)
  • Accessible date formatting using <time dateTime={...}>
  • Responsive design with appropriate breakpoints
  • Proper conditional rendering for optional metadata
  • Clean navigation with hover effects

The layout and user experience are solid.

apps/web/src/components/newsletter/Dropdown.tsx (1)

15-28: Overall dropdown behavior and a11y handling look solid.

Aside from the labelling and switch‑case nits above, the component is well put together: controlled value, outside‑click close, document‑level keyboard handling with Escape/Home/End, and use of role="listbox"/role="option" plus aria-selected and focus management all align nicely.

Also applies to: 79-106, 108-141

Comment on lines 90 to 92
aria-haspopup="listbox"
aria-expanded={open}
aria-labelledby="dropdown-label"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid hard‑coded aria-labelledby id; make the label configurable or remove it.

Using a fixed aria-labelledby="dropdown-label" in a reusable component is risky: multiple CustomDropdown instances will either share the same ID (invalid HTML) or reference a label that doesn’t exist, which hurts screen reader behavior.

One approach is to accept an optional labelId prop and wire it through:

-export default function CustomDropdown({
-  value,
-  onChange,
-  options,
-}: {
-  value: string;
-  onChange: (value: string) => void;
-  options: { label: string; value: string }[];
-}) {
+export default function CustomDropdown({
+  value,
+  onChange,
+  options,
+  labelId,
+}: {
+  value: string;
+  onChange: (value: string) => void;
+  options: { label: string; value: string }[];
+  labelId?: string;
+}) {
@@
-        aria-haspopup="listbox"
-        aria-expanded={open}
-        aria-labelledby="dropdown-label"
+        aria-haspopup="listbox"
+        aria-expanded={open}
+        aria-labelledby={labelId}
@@
-        <div
-          role="listbox"
-          aria-labelledby="dropdown-label"
+        <div
+          role="listbox"
+          aria-labelledby={labelId}

Call sites can then pass distinct IDs (or omit labelId if you prefer to rely on aria-label or the visible label inside the button).

Also applies to: 110-111

🤖 Prompt for AI Agents
In apps/web/src/components/newsletter/Dropdown.tsx around lines 90-92 (and
similarly 110-111), the component uses a hard-coded
aria-labelledby="dropdown-label" which can produce duplicate IDs or point to a
non-existent label; change the component to accept an optional prop (e.g.
labelId) and use that for aria-labelledby when provided, otherwise generate a
stable unique id internally (React's useId or a prop fallback) or fall back to
using aria-label/aria-describedby, and wire the corresponding label element to
that id at the call sites so each CustomDropdown instance references a unique,
valid label ID.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
apps/web/src/components/newsletter/Dropdown.tsx (1)

83-105: Add missing ARIA attributes to the trigger button.

The trigger button lacks aria-haspopup="listbox" and aria-expanded={open}, which are essential for screen readers to announce that this button opens a listbox and to convey whether it is currently expanded.

Apply this diff to add the required ARIA attributes:

       <button
         ref={buttonRef}
         onClick={() => setOpen(!open)}
+        aria-haspopup="listbox"
+        aria-expanded={open}
         onKeyDown={(e) => {
           if (e.key === "ArrowDown" || e.key === "ArrowUp") {
             e.preventDefault();
             setOpen(true);
           }
         }}
         className="
           w-full bg-black/20 border border-white/10 rounded-lg
           px-3 py-2 text-sm text-gray-200 flex items-center justify-between
           hover:bg-black/30 transition-all duration-200
           focus:outline-none focus:ring-2 focus:ring-blue-500/50
         "
       >
🧹 Nitpick comments (1)
apps/web/src/components/newsletter/Dropdown.tsx (1)

34-66: Consider guarding against empty options array.

While unlikely in practice, if options.length is 0, the modulo operations on lines 42 and 49 could produce unexpected results (NaN). Adding an early return improves defensive coding.

Add this guard at the top of handleKeyDown:

     function handleKeyDown(e: KeyboardEvent) {
+      if (options.length === 0) return;
+
       const currentIndex = optionsRef.current.findIndex(
         (el) => el === document.activeElement
       );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 30f4fbd and 4bb31b3.

📒 Files selected for processing (1)
  • apps/web/src/components/newsletter/Dropdown.tsx (1 hunks)

}
}, [open]);

const selected = options.find((opt) => opt.value === value);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Consider adding a fallback for invalid values.

If the value prop doesn't exist in the options array, the button will display nothing (line 99: selected?.label becomes undefined). This could occur if the parent passes an invalid value or the options array changes.

Consider adding a fallback:

       >
-        {selected?.label}
+        {selected?.label || "Select..."}
         {open ? (

Alternatively, validate the value prop or document that callers must ensure value exists in options.

Also applies to: 99-99

🤖 Prompt for AI Agents
In apps/web/src/components/newsletter/Dropdown.tsx around lines 79 and 99, the
selected lookup uses options.find(...) and then selected?.label is rendered,
which yields nothing when value isn't in options; add a fallback by resolving a
default label when selected is undefined (e.g., use a placeholder like
"Select..." or the first option's label) and ensure the component either
validates the value prop (type/check) or documents that callers must pass a
valid value; update the render to display that fallback and/or add a runtime
console warning or prop validation when an invalid value is detected.

Comment on lines +107 to +140
{open && (
<div
className="
absolute mt-2 w-full
bg-neutral-900 text-white
border border-white/10
rounded-md shadow-lg shadow-black/40
z-[9999]
"
>
{options.map((opt, index) => (
<button
type="button"
key={opt.value}
ref={(el) => { optionsRef.current[index] = el; }}
role="option"
aria-selected={opt.value === value}
onClick={() => {
onChange(opt.value);
setOpen(false);
buttonRef.current?.focus();
}}
className="
w-full text-left px-3 py-2 text-sm rounded-md text-gray-100 cursor-pointer
hover:bg-neutral-700 transition-all duration-200
focus:outline-none focus:bg-neutral-700
aria-selected:bg-neutral-800
"
>
{opt.label}
</button>
))}
</div>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add role="listbox" to the dropdown container.

The container div renders options with role="option" but lacks role="listbox" itself. Screen readers require the parent to have role="listbox" to properly announce the relationship between the container and its options.

Apply this diff to add the missing role:

       {open && (
         <div
+          role="listbox"
           className="
             absolute mt-2 w-full
             bg-neutral-900 text-white
             border border-white/10 
             rounded-md shadow-lg shadow-black/40
             z-[9999]
           "
         >
🤖 Prompt for AI Agents
In apps/web/src/components/newsletter/Dropdown.tsx around lines 107 to 140, the
dropdown container div that wraps the option buttons currently lacks an
accessible container role; add role="listbox" to that div (the element rendered
when open is true) so screen readers perceive the options' relationship; keep
the existing option buttons with role="option" unchanged and ensure the role
attribute is added directly on the outer div.

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4bb31b3 and 103b124.

📒 Files selected for processing (1)
  • apps/web/src/utils/newsletter/getAllNewsLetter.ts (1 hunks)
🔇 Additional comments (5)
apps/web/src/utils/newsletter/getAllNewsLetter.ts (5)

1-3: LGTM!

Imports are appropriate for reading and parsing markdown files with frontmatter.


5-13: LGTM!

Type definition is clear and matches the newsletter frontmatter structure described in the PR objectives.


26-32: LGTM!

The filtering logic correctly excludes example files and includes only .md files as intended.


41-45: LGTM!

The keyword normalization logic handles both array and comma-separated string formats with appropriate fallbacks.


17-24: Synchronous file operations are appropriate for this Server Component context.

Verification confirms that getAllNewsLetter() is called in a Next.js Server Component (no "use client" directive present in apps/web/src/app/(main)/dashboard/newsletters/page.tsx). In Server Components, synchronous file system operations are standard and acceptable practice, as they execute server-side during request handling without blocking the event loop for other concurrent requests.

Comment on lines +47 to +58
return {
id: (data.id as string | undefined) ?? slug,
title: (data.title as string | undefined) ?? "Untitled Newsletter",
summary: (data.summary as string | undefined) ?? "",
keywords,
time:
(data.time as string | undefined) ??
(data.date as string | undefined) ??
new Date().toISOString(),
readTime: (data.readTime as string | undefined) ?? "1 min read",
slug,
} satisfies Newsletter;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Time fallback may produce unexpected results.

Line 52-55: The fallback chain ends with new Date().toISOString(), meaning newsletters without time or date fields will get the current timestamp. This could lead to incorrect chronological ordering and confusing dates displayed to users.

Consider either:

  1. Making time/date required in frontmatter and throwing an error if missing
  2. Using a sentinel value (e.g., epoch zero) to identify newsletters with missing dates
  3. Logging a warning when falling back to the current date

Additionally, consider validating frontmatter with a schema library (e.g., zod) rather than relying on type assertions. This would catch malformed frontmatter early with clear error messages.

Apply this diff to add basic validation:

+        // Validate required fields
+        if (!data.time && !data.date) {
+          console.warn(`Newsletter ${filename} is missing time/date field`);
+        }
+
         return {

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

🤖 Prompt for AI Agents
In apps/web/src/utils/newsletter/getAllNewsLetter.ts around lines 47 to 58, the
current fallback for time/date uses new Date().toISOString() which can produce
misleading current timestamps for newsletters missing frontmatter; replace this
by validating the frontmatter and handling missing dates explicitly: add a
lightweight validation (preferably with zod) that requires either time or date
and fails the build or throws a clear error when both are missing, or if you
prefer non-blocking behavior set time to a sentinel value (e.g., new
Date(0).toISOString()) and emit a console/processLogger.warn when falling back
so consumers can detect missing dates; update the returned object to use the
validated/normalized value (or sentinel) instead of defaulting to now and stop
using unchecked type assertions for frontmatter.

Comment on lines +59 to +61
} catch {
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error logging for debugging.

The empty catch blocks silently swallow all errors, making it difficult to diagnose issues with newsletter files (malformed frontmatter, encoding problems, missing fields, etc.). This will make debugging and content authoring harder for the team.

Apply this diff to add error logging:

-        } catch {
+        } catch (error) {
+          console.error(`Failed to parse newsletter file ${filename}:`, error);
           return null;
         }
       })
       .filter((newsletter): newsletter is Newsletter => newsletter !== null);
-  } catch {
+  } catch (error) {
+    console.error('Failed to read newsletter directory:', error);
     return [];
   }

Also applies to: 64-66

🤖 Prompt for AI Agents
In apps/web/src/utils/newsletter/getAllNewsLetter.ts around lines 59-61 and
64-66, the catch blocks silently swallow errors; modify each catch to accept the
error (catch (error)) and log the error before returning null — e.g., call your
project logger or console.error with a clear message including the newsletter
file path or identifier and the error object (message/stack) to aid debugging,
then return null as before.

return null;
}
})
.filter((newsletter): newsletter is Newsletter => newsletter !== null);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing required sorting by date.

The PR objectives and issue #155 explicitly require newsletters to be "ordered by date (latest first)", but this function returns newsletters in filesystem order without sorting. This means the newsletter listing page won't display items in the correct chronological order.

Apply this diff to sort newsletters by date descending:

       .filter((newsletter): newsletter is Newsletter => newsletter !== null);
+      .sort((a, b) => new Date(b.time).getTime() - new Date(a.time).getTime());
   } catch {

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

🤖 Prompt for AI Agents
In apps/web/src/utils/newsletter/getAllNewsLetter.ts around line 63, after
filtering out null newsletters, the list is not being sorted; update the
pipeline to sort newsletters by their date field in descending order (latest
first) — e.g. after the .filter(...) call, add a .sort(...) that compares parsed
Date values (or timestamps) from newsletter.date, handling missing/invalid dates
by treating them as oldest so they appear last.

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/hooks/useNewsletterFilters.ts (1)

46-56: Add date validation in month filtering logic.

Similar to the month options generation, lines 50-54 create a Date and format it without validating whether the date is valid. Invalid dates will cause incorrect filtering behavior.

Apply this diff to add validation:

     // Filter by month
     const filteredByMonth =
       selectedMonth === "all"
         ? filtered
         : filtered.filter((item) => {
             const date = new Date(item.time);
+            // Skip items with invalid dates
+            if (isNaN(date.getTime())) {
+              return false;
+            }
             const monthYear = date.toLocaleDateString("en-US", {
               month: "long",
               year: "numeric",
             });
             return monthYear === selectedMonth;
           });
🧹 Nitpick comments (1)
apps/web/src/hooks/useNewsletterFilters.ts (1)

12-56: Consider extracting date formatting logic to reduce duplication.

The pattern of creating a Date and formatting it to "Month Year" appears multiple times (lines 15-19 and 50-54). Extracting this to a helper function would reduce duplication and make future maintenance easier.

For example:

function getMonthYear(timeString: string): string | null {
  const date = new Date(timeString);
  if (isNaN(date.getTime())) {
    return null;
  }
  return date.toLocaleDateString("en-US", {
    month: "long",
    year: "numeric",
  });
}

Then use it in both places:

// In monthOptions generation
const monthYear = getMonthYear(item.time);
if (monthYear) months.add(monthYear);

// In month filtering
const monthYear = getMonthYear(item.time);
if (!monthYear) return false;
return monthYear === selectedMonth;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 103b124 and f0ac259.

📒 Files selected for processing (2)
  • apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx (1 hunks)
  • apps/web/src/hooks/useNewsletterFilters.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/src/hooks/useNewsletterFilters.ts (1)
apps/web/src/utils/newsletter/getAllNewsLetter.ts (1)
  • Newsletter (5-13)
🔇 Additional comments (4)
apps/web/src/hooks/useNewsletterFilters.ts (4)

1-4: LGTM - clean imports and type definition.

The import of Newsletter type from the utility file follows the single source of truth pattern, and the SortOrder type provides good type safety for the sort options.


58-68: LGTM - sort logic properly handles invalid dates.

The sort logic correctly validates dates using isNaN(dateA) || isNaN(dateB) before comparing, preventing crashes from malformed date strings.


33-43: LGTM - comprehensive search filter implementation.

The search filter appropriately checks across title, summary, and keywords with case-insensitive matching, providing a good user experience.


71-92: LGTM - clean utility functions and comprehensive return interface.

The clearFilters function properly resets state, hasActiveFilters appropriately excludes sort order (since sorting isn't technically filtering), and the return statement exposes a well-structured API for consuming components.

Comment on lines +12 to +29
const monthOptions = useMemo(() => {
const months = new Set<string>();
initialData.forEach((item) => {
const date = new Date(item.time);
const monthYear = date.toLocaleDateString("en-US", {
month: "long",
year: "numeric",
});
months.add(monthYear);
});

return [
{ label: "All Months", value: "all" },
...Array.from(months)
.sort((a, b) => new Date(b).getTime() - new Date(a).getTime())
.map((month) => ({ label: month, value: month })),
];
}, [initialData]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add missing date validation in month options generation.

Despite the previous review comment being marked as addressed, the current code at lines 15-19 does not validate whether item.time produces a valid Date. If new Date(item.time) returns an Invalid Date, toLocaleDateString returns "Invalid Date" as a string, which pollutes the month options dropdown.

Additionally, line 26 assumes month strings like "November 2025" parse correctly, which may fail with unexpected formats.

Apply this diff to add validation:

   const monthOptions = useMemo(() => {
     const months = new Set<string>();
     initialData.forEach((item) => {
       const date = new Date(item.time);
+      // Skip invalid dates
+      if (isNaN(date.getTime())) {
+        console.warn(`Invalid date in newsletter: ${item.id || item.slug}`);
+        return;
+      }
       const monthYear = date.toLocaleDateString("en-US", {
         month: "long",
         year: "numeric",
       });
       months.add(monthYear);
     });

     return [
       { label: "All Months", value: "all" },
       ...Array.from(months)
         .sort((a, b) => new Date(b).getTime() - new Date(a).getTime())
         .map((month) => ({ label: month, value: month })),
     ];
   }, [initialData]);
🤖 Prompt for AI Agents
In apps/web/src/hooks/useNewsletterFilters.ts around lines 12-29, the code
currently creates month options without validating dates and later sorts by
constructing new Dates from the month label which may fail; update the
generation to (1) for each item, create a Date from item.time and only add month
strings when date is valid (e.g., check isFinite(date.getTime()) or
!isNaN(date.getTime())) so "Invalid Date" is never inserted, and (2) when
sorting the unique month labels, do not rely on parsing the label directly with
new Date(label); instead parse the label into month name and year (or map month
names to month indexes) and construct a deterministic Date (e.g., new Date(year,
monthIndex, 1) or Date.UTC) to sort by timestamp descending before mapping to
{label,value} entries.

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

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

4-8: Consider error handling for better debugging.

While getAllNewsLetter() has internal error handling that returns an empty array on failure (preventing crashes), the page has no visibility into whether the empty state is due to no newsletters or a loading error. This makes debugging content issues harder.

Consider logging or distinguishing between empty and error states:

 export default function Page() {
   const newsletters = getAllNewsLetter();
+  
+  // Optional: Log for debugging if empty (could indicate an error)
+  if (newsletters.length === 0) {
+    console.log('[Newsletter Page] No newsletters loaded - check content/newsletters directory');
+  }

   return <NewsletterPage initialData={newsletters} />;
 }
apps/web/src/utils/newsletter/newsletter-metadata.ts (4)

17-67: Consider adding default date-based sorting.

While the UI (NewsletterPage) handles sorting via useNewsletterFilters, having a default sort order here ensures newsletters are consistently ordered regardless of where getAllNewsLetter() is called. This would align with the PR objective of "ordered by date (latest first)".

Apply this diff to add default sorting:

       .filter((newsletter): newsletter is Newsletter => newsletter !== null);
+      .sort((a, b) => {
+        const dateA = new Date(a.time).getTime();
+        const dateB = new Date(b.time).getTime();
+        return dateB - dateA; // Latest first
+      });
   } catch (error) {

59-61: Add error logging for debugging malformed newsletter files.

The empty catch block silently swallows all parsing errors (malformed frontmatter, encoding issues, missing fields), making it difficult to diagnose issues when adding new newsletter content.

Apply this diff:

-        } catch {
+        } catch (error) {
+          console.error(`Failed to parse newsletter file ${filename}:`, error);
           return null;
         }

64-66: Add error logging for directory read failures.

The empty catch block hides directory-level errors, making it hard to debug when the newsletter directory is misconfigured or inaccessible.

Apply this diff:

-  } catch {
+  } catch (error) {
+    console.error('Failed to read newsletter directory:', error);
     return [];
   }

52-55: Time fallback may produce misleading timestamps.

Using new Date().toISOString() as a fallback means newsletters missing time or date fields will get the current timestamp, leading to incorrect chronological ordering and confusing dates in the UI.

Consider one of these approaches:

  1. Validate and require time/date (recommended):
+        if (!data.time && !data.date) {
+          console.error(`Newsletter ${filename} is missing required time/date field`);
+          return null;
+        }
+
         return {
           id: (data.id as string | undefined) ?? slug,
           title: (data.title as string | undefined) ?? "Untitled Newsletter",
           summary: (data.summary as string | undefined) ?? "",
           keywords,
           time:
             (data.time as string | undefined) ??
-            (data.date as string | undefined) ??
-            new Date().toISOString(),
+            (data.date as string | undefined)!,
  1. Or use a sentinel value to mark missing dates:
           time:
             (data.time as string | undefined) ??
             (data.date as string | undefined) ??
-            new Date().toISOString(),
+            new Date(0).toISOString(), // Epoch zero signals missing date
🧹 Nitpick comments (1)
apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx (1)

48-59: Add aria-label to search input for accessibility.

The search input is missing an aria-label attribute, which is important for screen reader users since the placeholder text is not read as a label.

Apply this diff:

          <input
            type="text"
            placeholder="Search newsletter..."
+           aria-label="Search newsletters"
            value={search}
            onChange={(e) => setSearch(e.target.value)}
            className="
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f0ac259 and 2fa2f36.

📒 Files selected for processing (7)
  • apps/web/content/newsletters/example.md (1 hunks)
  • apps/web/content/newsletters/test-1.md (1 hunks)
  • apps/web/content/newsletters/test-2.md (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/page.tsx (1 hunks)
  • apps/web/src/hooks/useNewsletterFilters.ts (1 hunks)
  • apps/web/src/utils/newsletter/newsletter-metadata.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/src/hooks/useNewsletterFilters.ts
  • apps/web/content/newsletters/example.md
🧰 Additional context used
🧬 Code graph analysis (2)
apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx (4)
apps/web/src/utils/newsletter/newsletter-metadata.ts (1)
  • Newsletter (5-13)
apps/web/src/hooks/useNewsletterFilters.ts (1)
  • useNewsletterFilters (6-93)
apps/web/src/components/newsletter/Dropdown.tsx (1)
  • CustomDropdown (6-143)
apps/web/src/components/newsletter/NewsletterItem.tsx (1)
  • NewsletterItem (13-86)
apps/web/src/app/(main)/dashboard/newsletters/page.tsx (2)
apps/web/src/utils/newsletter/newsletter-metadata.ts (1)
  • getAllNewsLetter (17-67)
apps/web/src/app/(main)/dashboard/newsletters/NewsletterPage.tsx (1)
  • NewsletterPage (9-130)
🪛 LanguageTool
apps/web/content/newsletters/test-2.md

[style] ~30-~30: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...till publish it and let people use it. When your resume looks like a blank slate, b...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

apps/web/content/newsletters/test-1.md

[style] ~28-~28: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...ing. No college degree. No connections. No safety net. Just documentation, tutoria...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~61-~61: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...mission. You don't need a fancy degree. You need a computer, internet, and the refu...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🔇 Additional comments (3)
apps/web/content/newsletters/test-1.md (1)

1-63: LGTM! Well-structured newsletter content.

The frontmatter correctly follows the Newsletter schema and the content is well-organized with proper Markdown formatting. The repetitive sentence structures flagged by static analysis (lines 28 and 61) are intentional rhetorical devices (anaphora) used for emphasis and don't need to be changed.

apps/web/content/newsletters/test-2.md (1)

1-68: LGTM! Engaging newsletter content with proper structure.

The frontmatter follows the Newsletter schema correctly and the content is well-formatted. The repetitive "When" structure flagged by static analysis (line 30) is an intentional stylistic choice (anaphora) that enhances the message and doesn't require changes.

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

9-129: Well-structured component with comprehensive filtering and responsive design.

The component effectively integrates filtering, sorting, and responsive layouts. The empty state handling with conditional "Clear all filters" action provides good UX.

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

🧹 Nitpick comments (1)
apps/web/src/components/dashboard/Sidebar.tsx (1)

64-66: Newsletter navigation handler is fine (minor naming nit only)

handleNewsLetter correctly routes to /dashboard/newsletters. If you touch this again, consider renaming to handleNewsletter for consistency with the route and label, but this is purely optional.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2fa2f36 and e76e435.

📒 Files selected for processing (2)
  • apps/web/src/components/dashboard/Sidebar.tsx (3 hunks)
  • apps/web/tailwind.config.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/tailwind.config.ts
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/src/components/dashboard/Sidebar.tsx (2)
apps/web/src/components/sheet/OpensoxProBadge.tsx (1)
  • OpensoxProBadge (8-28)
apps/web/src/components/sidebar/SidebarItem.tsx (1)
  • SidebarItem (13-42)
🔇 Additional comments (1)
apps/web/src/components/dashboard/Sidebar.tsx (1)

24-24: Mailbox icon import is consistent with existing icon usage

Importing Mailbox from lucide-react aligns with how other lucide icons (e.g., ChevronLeftIcon, ChevronRightIcon) are used; no issues here.

Comment on lines +136 to +158
{!isCollapsed && !isPaidUser ? (
<div
className="w-full h-[44px] flex items-center rounded-md cursor-pointer transition-colors px-2 gap-3 pl-3 hover:bg-[#121214]"
onClick={handleNewsLetter}
>
<span className="shrink-0 text-[#eaeaea]">
<Mailbox className="size-5" />
</span>
<div className="flex items-center gap-1">
<h1 className="text-xs font-medium text-[#c8c8c8] group-hover:text-ox-purple">
Newsletter
</h1>
<OpensoxProBadge className="px-1.5 py-0.5 scale-75" />
</div>
</div>
) : (
<SidebarItem
itemName="Newsletter"
onclick={handleNewsLetter}
icon={<Mailbox className="size-5" />}
collapsed={isCollapsed}
/>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Badge click likely conflicts with parent onClick; consider refactor to reuse SidebarItem

  1. Conflicting navigation on badge click (minor UX bug)
    In the expanded, non‑paid case, the parent <div> uses onClick={handleNewsLetter} (routing to /dashboard/newsletters), while OpensoxProBadge renders a nested <Link href="/pricing">. Clicking the badge will trigger both handlers; the final navigation is likely /dashboard/newsletters, so the badge won’t reliably act as a pricing link. Consider either:

    • Removing the nested Link for this specific usage and routing to pricing via a click handler that calls event.stopPropagation() and then router.push("/pricing"), or
    • Keeping the badge purely decorative here (no Link) and relying on the newsletter page to handle the upgrade flow.

    Please manually test clicking exactly on the “OX Pro” badge inside the Newsletter row and confirm whether you land on /pricing or /dashboard/newsletters.

  2. Reuse SidebarItem for consistency and less duplication
    The custom <div> duplicates SidebarItem’s layout/hover behavior and partially diverges from existing design tokens (hard‑coded hex colors instead of text-text-* and hover:bg-dash-hover). Since SidebarItem already accepts a badge prop, you could simplify to something like:

    <SidebarItem
      itemName="Newsletter"
      onclick={handleNewsLetter}
      icon={<Mailbox className="size-5" />}
      collapsed={isCollapsed}
      badge={!isPaidUser ? <OpensoxProBadge className="px-1.5 py-0.5 scale-75" /> : undefined}
    />

    and then conditionally render the whole block based on isPaidUser as you do now. This keeps styling consistent and avoids a custom one‑off row.

  3. Minor Tailwind group-hover issue
    The heading uses group-hover:text-ox-purple but the root <div> doesn’t include the group class, so that hover style won’t take effect. Either add group to the root or change to a simple hover: modifier on the text as desired.



---

🏁 Script executed:

```shell
#!/bin/bash
# Find and examine the OpensoxProBadge component
fd -t f "OpensoxProBadge" --exec cat {} \;

Length of output: 759


🏁 Script executed:

#!/bin/bash
# Search for OpensoxProBadge definition
rg -n "export.*OpensoxProBadge\|const OpensoxProBadge\|function OpensoxProBadge" -A 20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Examine SidebarItem component to check props
rg -n "export.*SidebarItem\|const SidebarItem\|function SidebarItem" -A 30 --type=tsx

Length of output: 27


🏁 Script executed:

#!/bin/bash
# View the full Sidebar.tsx file around the target lines
cat apps/web/src/components/dashboard/Sidebar.tsx | head -170 | tail -50

Length of output: 1959


🏁 Script executed:

#!/bin/bash
# Check for design token definitions (text-text-*, hover:bg-dash-hover)
rg -n "text-text-|hover:bg-dash-hover|@apply.*text-text" --type=tsx --type=css | head -20

Length of output: 27



---

🏁 Script executed:

```shell
#!/bin/bash
# Find SidebarItem file without type filter
fd -t f "SidebarItem" --exec cat {} \;

Length of output: 1007


🏁 Script executed:

#!/bin/bash
# Search for SidebarItem definition with broader pattern
rg -n "SidebarItem\s*=" -A 30 | head -60

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for prop interfaces or type definitions for SidebarItem
rg -n "interface.*SidebarItem|type.*SidebarItem" -A 15

Length of output: 1093


🏁 Script executed:

#!/bin/bash
# Check if SidebarItem accepts badge prop anywhere in usage or definition
rg -n "badge\s*=" -C 3 | grep -A 3 -B 3 "SidebarItem"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check full newsletter section in context
sed -n '130,170p' apps/web/src/components/dashboard/Sidebar.tsx

Length of output: 1703


Fix badge click event handling and CSS styling inconsistencies in Newsletter section

The expanded, non-paid Newsletter row has three issues:

  1. Event handler conflict on badge click: OpensoxProBadge wraps its content in <Link href="/pricing"> without calling event.stopPropagation(). Clicking the badge will fire both the parent onClick={handleNewsLetter} and the nested Link navigation, likely landing on /dashboard/newsletters instead of /pricing. Add event.stopPropagation() inside OpensoxProBadge or use router.push() with a click handler that prevents default behavior.

  2. Missing group class: The root <div> lacks the group class, so the group-hover:text-ox-purple modifier on the heading won't work. Add group to the root div className.

  3. Design token mismatch: Replace hardcoded hex colors (#eaeaea, #c8c8c8, #121214) with design tokens already used elsewhere in the sidebar (text-text-secondary, text-text-tertiary, hover:bg-dash-hover). Also, consider using SidebarItem with the badge prop instead of a custom div to maintain consistency:

<SidebarItem
  itemName="Newsletter"
  onclick={handleNewsLetter}
  icon={<Mailbox className="size-5" />}
  collapsed={isCollapsed}
  badge={!isPaidUser ? <OpensoxProBadge className="px-1.5 py-0.5 scale-75" /> : undefined}
/>
🤖 Prompt for AI Agents
In apps/web/src/components/dashboard/Sidebar.tsx around lines 136-158, the
expanded non-paid Newsletter row needs three fixes: ensure the OpensoxProBadge
click does not bubble to the parent by adding event.stopPropagation() in the
badge’s click handler or converting the badge to a button that calls
router.push('/pricing') while preventing propagation; add the "group" class to
the root div so group-hover styles on the heading take effect; and replace
hard‑coded hex colors (#eaeaea, #c8c8c8, #121214) with existing design tokens
(text-text-secondary, text-text-tertiary, hover:bg-dash-hover) for consistency —
alternatively simplify by using the SidebarItem component with a badge prop when
!isPaidUser to keep behavior and styling consistent.

@apsinghdev
Copy link
Owner

@deveshru2712 thanks for the submission! Unfortunately, we are moving with a different submission this time, so we won't be able to accept it. Still, you are welcome to make contributions! 🙏

@apsinghdev apsinghdev closed this Nov 20, 2025
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

2 participants