Skip to content

Conversation

@dakshydv
Copy link

@dakshydv dakshydv commented Nov 14, 2025

fix: #155

  • Added newsletters dashboard only accessible to premium users
  • Added individual newsletter's page
  • Implement search functionality
  • Added sample newsletters for testing
  • Stored newsletter content in web/src/lib/newsletter.ts
Screen.Recording.2025-11-14.at.5.40.22.PM.mov

Summary by CodeRabbit

Release Notes

  • New Features
    • Introduced a new Newsletter section in the dashboard, accessible from the sidebar navigation.
    • View a curated list of newsletters with live search functionality to filter by title and content.
    • Read individual newsletter articles with rich formatting support including code blocks, images, headings, and links.
    • Premium feature: Newsletter access restricted to paid users.

@vercel
Copy link

vercel bot commented Nov 14, 2025

@dakshydv 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.

@cla-assistant
Copy link

cla-assistant bot commented Nov 14, 2025

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 14, 2025

Walkthrough

Implements a complete newsletter feature for the dashboard with list and article page components, rich markdown rendering supporting embeds and images, newsletter data utilities, card-based UI components, premium user gating, and full documentation on content management.

Changes

Cohort / File(s) Summary
Newsletter Pages
apps/web/src/app/(main)/dashboard/newsletter/page.tsx, apps/web/src/app/(main)/dashboard/newsletter/[id]/page.tsx
Added client-side list page with search, filtering, premium gating, and loading states; added server-side article page with markdown rendering for inline tags, bold text, links, images, code blocks with embeds, lists, blockquotes, and headings.
Newsletter Components
apps/web/src/components/newsletter/newsletter-card.tsx, apps/web/src/components/newsletter/newsletter-list.tsx
Introduced newsletter card component displaying title, date with icon, and optional excerpt; introduced newsletter list component grouping items by month.
Newsletter Utilities & Data
apps/web/src/lib/newsletters.ts
Added newsletter type definition and array of sample data; exported utilities for retrieving single/all newsletters and grouping by month.
Navigation & Styling
apps/web/src/components/dashboard/Sidebar.tsx, apps/web/src/app/globals.css
Added newsletter route to sidebar with Newspaper icon; added CSS layer for prose styling (links, inline code, headings, paragraphs).
Documentation
docs/NEWSLETTER.md
Added guide on newsletter data structure, supported markdown content, helper functions, and step-by-step instructions for adding newsletter entries.

Sequence Diagrams

sequenceDiagram
    participant User
    participant NewsletterPage
    participant useSubscription
    participant getAllNewslettersSorted
    participant useMemo
    participant NewsletterList

    User->>NewsletterPage: Visit /dashboard/newsletter
    NewsletterPage->>getAllNewslettersSorted: Fetch all newsletters
    getAllNewslettersSorted-->>NewsletterPage: Return sorted list
    NewsletterPage->>useSubscription: Check user subscription
    alt User is not paid
        NewsletterPage->>NewsletterPage: Show premium gate modal
    else User is paid
        NewsletterPage->>useMemo: Filter newsletters by query
        useMemo-->>NewsletterPage: Return filtered items
        NewsletterPage->>NewsletterList: Render with filtered items
        NewsletterList-->>User: Display grouped by month
    end
Loading
sequenceDiagram
    participant User
    participant ArticlePage
    participant getNewsletterById
    participant Markdown Renderer
    participant User (Display)

    User->>ArticlePage: Visit /dashboard/newsletter/[id]
    ArticlePage->>getNewsletterById: Fetch newsletter by ID
    alt Newsletter found
        getNewsletterById-->>ArticlePage: Return newsletter
        ArticlePage->>Markdown Renderer: Parse markdown content
        rect rgb(230, 240, 255)
            note right of Markdown Renderer: Process: headings, bold,<br/>links, images, code blocks,<br/>embeds, lists, blockquotes
        end
        Markdown Renderer-->>ArticlePage: Rendered content
        ArticlePage-->>User (Display): Display article with back nav
    else Newsletter not found
        getNewsletterById-->>ArticlePage: Return undefined
        ArticlePage-->>User (Display): Show not found
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Specific areas requiring attention:
    • Markdown renderer logic in article page: multiple content type handlers with URL validation and embed processing
    • Premium user gating logic in list page: ensure subscription state is correctly assessed and applied
    • Content grouping utility (groupByMonth): verify date parsing and group ordering
    • CSS prose styling layer: confirm selector specificity and styling consistency with design system

Possibly related issues

  • [bounty-to-hire]: create ui for newsletter #155: This PR directly implements the bounty-to-hire newsletter feature requirements, including organization by month, rich content support (text, links, images, bold, headings), code-based content management with documentation, and a premium-only newsletter listing page.

Poem

🐰 Hop into newsletters, dear friend!
With markdown tails and embeds to blend,
Month-grouped prose, both bold and bright,
Premium gates and reading delight—
A fuzzy feature, start to end! 📰

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Implement Newsletter dashboard' accurately reflects the main changes, which add a complete newsletter feature with pages, search, and styling.
Linked Issues check ✅ Passed The PR implements all core requirements: organization by date via groupByMonth [#155], rich content support with markdown renderer [#155], content management through code in newsletters.ts with documentation [#155], newsletter listing with sorting [#155], and three sample newsletters [#155].
Out of Scope Changes check ✅ Passed All changes directly support the newsletter feature. CSS updates style prose elements for markdown rendering, sidebar navigation enables newsletter access, and components form the complete UI implementation as specified in the requirements.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (4)
docs/NEWSLETTER.md (1)

22-30: Tighten markdown examples and lint issues

Two small doc nits here:

  • The inline code and code‑fence examples include extra backticks/spaces (e.g. Inline code: `` \example` , `Code blocks: triple backticks ( )), which triggers MD038 (“spaces inside code span elements”) and can be simplified to standard forms like ``Inline code: example `` and “Code blocks: triple backticks ( ``` `)”.
  • The phrase “Full markdown is supported in practice” is a bit stronger than what the custom renderer actually handles (it covers headings, backticks, bold, links, images, - lists, quotes, and fenced code, but not all markdown features). Consider softening this to “The renderer supports common markdown features used in our newsletters (headings, code fences, inline code, bold/italic, links, images, lists, etc.).”
apps/web/src/components/newsletter/newsletter-list.tsx (1)

8-21: Remove commented‑out duplicate groupByMonth implementation

The commented local groupByMonth implementation duplicates the logic already exported from @/lib/newsletters. Keeping the single shared helper avoids divergence and keeps this component focused.

Recommend deleting the commented block to reduce noise.

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

9-20: Optimize newsletter loading and make search case‑insensitive

Two small improvements to the search/filter logic:

  1. useMemo is ineffective right now
    const items = getAllNewslettersSorted(); creates a new array each render, so items always changes identity and the useMemo recomputes every time anyway. Either:

    • Hoist the call into a useMemo with an empty dep array, or
    • Compute items once outside the component, or
    • Drop useMemo on filtered and just compute directly (the dataset is small).
  2. Search is case‑sensitive
    hay.includes(q) will miss matches where casing differs (“AI” vs “ai”). Normalizing both sides will give a more forgiving UX:

-  const filtered = useMemo(() => {
-    const q = query.trim();
+  const filtered = useMemo(() => {
+    const q = query.trim().toLowerCase();
     if (!q) return items;
     return items.filter((it) => {
-      const hay = `${it.title} ${it.excerpt ?? ""} ${it.content}`;
-      return hay.includes(q);
+      const hay = `${it.title} ${it.excerpt ?? ""} ${it.content}`.toLowerCase();
+      return hay.includes(q);
     });
-  }, [items, query]);
+  }, [items, query]);
apps/web/src/app/(main)/dashboard/newsletter/[id]/page.tsx (1)

74-283: Ensure final blockquotes are flushed and consider premium gating for deep links

Two related but independent points:

  1. Final blockquote edge case
    blockquoteBuffer is flushed only when you hit a blank line, an image, or a paragraph boundary. If a newsletter ends with a blockquote and no trailing blank line/content, those lines will remain in blockquoteBuffer and never get rendered. A minimal fix is to flush blockquotes after the main loop:
  lines.forEach((raw, idx) => {
    // existing logic...
  });

-  flushList();
-  return nodes;
+  flushList();
+  if (blockquoteBuffer.length) {
+    nodes.push(
+      <blockquote
+        key={`bq-end-${nodes.length}`}
+        className="border-l-4 pl-4 italic text-ox-white/80 my-3"
+      >
+        {blockquoteBuffer.map((l, i) => (
+          <p key={i} className="m-0 py-1">
+            {renderInline(l.replace(/^>\s?/, ""))}
+          </p>
+        ))}
+      </blockquote>
+    );
+  }
+  return nodes;
  1. Premium gating of article deep links (verify requirement)
    The listing page (/dashboard/newsletter) explicitly gates access with useSubscription, but this detail page renders any getNewsletterById(id) result without checking subscription. If the business requirement is “newsletter content is premium‑only even when deep‑linked”, you may want to mirror the subscription gating here (either via a client wrapper or a server‑side check) so non‑paid users can’t access articles directly by URL.

If /dashboard already applies its own premium guard, this might be fine as‑is, but it’s worth double‑checking the access story.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5445fd9 and 371a260.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • apps/web/src/app/(main)/dashboard/newsletter/[id]/page.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletter/page.tsx (1 hunks)
  • apps/web/src/app/globals.css (1 hunks)
  • apps/web/src/components/dashboard/Sidebar.tsx (2 hunks)
  • apps/web/src/components/newsletter/newsletter-card.tsx (1 hunks)
  • apps/web/src/components/newsletter/newsletter-list.tsx (1 hunks)
  • apps/web/src/lib/newsletters.ts (1 hunks)
  • docs/NEWSLETTER.md (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
apps/web/src/components/dashboard/Sidebar.tsx (1)
apps/web/src/app/(main)/dashboard/layout.tsx (1)
  • DashboardLayout (9-35)
apps/web/src/lib/newsletters.ts (2)
apps/web/src/data/blogs.ts (1)
  • BlogPost (8-13)
apps/web/src/app/(main)/blogs/page.tsx (2)
  • a (24-30)
  • blog (63-80)
apps/web/src/app/(main)/dashboard/newsletter/[id]/page.tsx (1)
apps/web/src/lib/newsletters.ts (1)
  • getNewsletterById (265-267)
apps/web/src/components/newsletter/newsletter-list.tsx (2)
apps/web/src/lib/newsletters.ts (2)
  • newsletter (1-7)
  • groupByMonth (273-286)
apps/web/src/components/newsletter/newsletter-card.tsx (1)
  • NewsletterCard (11-41)
apps/web/src/app/(main)/dashboard/newsletter/page.tsx (3)
apps/web/src/lib/newsletters.ts (1)
  • getAllNewslettersSorted (269-271)
apps/web/src/hooks/useSubscription.ts (1)
  • useSubscription (11-75)
apps/web/src/components/newsletter/newsletter-list.tsx (1)
  • NewsletterList (23-47)
🪛 markdownlint-cli2 (0.18.1)
docs/NEWSLETTER.md

29-29: Spaces inside code span elements

(MD038, no-space-in-code)

🔇 Additional comments (2)
apps/web/src/components/dashboard/Sidebar.tsx (1)

26-43: Newsletter sidebar entry wiring looks good

The new Newspaper icon import and /dashboard/newsletter route entry are consistent with existing sidebar patterns; no issues from a routing/UX perspective.

apps/web/src/app/globals.css (1)

76-104: Prose styling aligns with newsletter use‑case

The added .prose styles (links, inline code, paragraph spacing, heading colors) are cohesive with the newsletter article layout and should improve readability without clashing with Tailwind Typography defaults.

Comment on lines +110 to +151
if (lang === "embed") {
// embed block expects a URL in the content
const src = codeContent.trim();
try {
const url = new URL(src);
if (!["http:", "https:"].includes(url.protocol)) {
console.warn("Invalid embed URL protocol:", src);
return; // skip this embed
}
} catch {
console.warn("Invalid embed URL:", src);
}
nodes.push(
<div key={`embed-${idx}`} className="my-4 w-full">
<div className="relative" style={{ paddingTop: "56.25%" }}>
<iframe
src={src}
title={src}
className="absolute top-0 left-0 w-full h-full rounded-md"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
</div>
</div>
);
} else {
nodes.push(
<pre
key={`code-${idx}`}
className="my-4 overflow-auto rounded-md bg-[#0b0b0c] p-3 text-sm"
>
<code>{codeContent}</code>
</pre>
);
}
// replace consumed lines with empties so outer loop sees them as blank and skips
for (let k = idx; k <= j; k++) {
lines[k] = "__CONSUMED__";
}
return;
}
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

🧩 Analysis chain

Tighten embed handling to truly skip invalid URLs

In the lang === "embed" branch:

  • You correctly validate protocol for successfully parsed URLs and return early when it’s not http/https.
  • In the catch block, you log console.warn("Invalid embed URL:", src); but then still proceed to push an <iframe src={src} />, so invalid/relative values are rendered despite the warning.

If the intent is “only render embeds for valid HTTP(S) URLs”, consider:

      if (lang === "embed") {
        // embed block expects a URL in the content
        const src = codeContent.trim();
        try {
          const url = new URL(src);
          if (!["http:", "https:"].includes(url.protocol)) {
            console.warn("Invalid embed URL protocol:", src);
-            return; // skip this embed
+            return; // skip this embed
          }
        } catch {
          console.warn("Invalid embed URL:", src);
-        }
-        nodes.push(
+          return; // skip this embed
+        }
+        nodes.push(

That way, any URL that fails parsing or protocol checks is consistently skipped.


I'll verify the review comment by inspecting the actual code at the specified location.


---

🏁 Script executed:

```shell
#!/bin/bash
# Inspect the actual code at the specified file and line range
fd --type f 'page.tsx' | grep -E 'newsletter/\[id\]/page\.tsx' | head -1 | xargs cat | sed -n '100,160p' | cat -n

Length of output: 2435


Add return statement in catch block to skip invalid embed URLs

The review comment is correct. The current code validates the protocol only after successful URL parsing, but if new URL(src) throws in the catch block, the warning is logged but the iframe still renders with the invalid src value.

The suggested fix is sound: add return; after the console.warn in the catch block (line 22) to ensure that any URL failing to parse is consistently skipped, not just URLs with invalid protocols. This prevents rendering iframes with unparseable or relative URLs.

🤖 Prompt for AI Agents
In apps/web/src/app/(main)/dashboard/newsletter/[id]/page.tsx around lines 110
to 151, the catch block for new URL(src) logs an invalid-URL warning but does
not return, so the iframe still renders for unparseable or relative src values;
update the catch block to immediately return after console.warn("Invalid embed
URL:", src) so invalid URLs are skipped consistently (mirror the earlier
protocol-check return) and ensure consumed-line replacement still happens by
keeping the existing post-processing and return outside the try/catch.

Comment on lines +12 to +39
const formattedDate = new Date(date).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
return (
<Link
href={`/dashboard/newsletter/${id}`}
className="border border-ox-gray hover:cursor-pointer w-full max-w-sm rounded-lg p-5 bg-ox-black-2 hover:border-ox-purple-2 transition-colors"
>
<div className="flex items-center gap-2 text-sm mb-3 text-ox-gray">
<Calendar className="w-4 h-4 text-ox-gray" />
<span>{formattedDate}</span>
</div>
<h3 className="text-lg md:text-xl font-semibold text-ox-white mb-2 leading-tight">
<Link
href={`/dashboard/newsletter/${id}`}
className="hover:text-ox-purple-2"
>
{title}
</Link>
</h3>
{excerpt && (
<p className="text-sm text-gray-300 leading-relaxed line-clamp-3">
{excerpt}
</p>
)}
</Link>
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

Fix date parsing and avoid nested <Link> elements

Two issues here:

  1. Potential off‑by‑one dates with new Date("YYYY-MM-DD")
    new Date("2025-11-08") is interpreted as midnight UTC, so in many timezones the localized date will render as the previous day. Since your date field is a plain yyyy-mm-dd, it’s safer to construct the date in local time:
-  const formattedDate = new Date(date).toLocaleDateString("en-US", {
+  const [year, month, day] = date.split("-").map(Number);
+  const formattedDate = new Date(year, month - 1, day).toLocaleDateString("en-US", {
     year: "numeric",
     month: "long",
     day: "numeric",
   });
  1. Nested Link components
    The outer Link already renders an <a>; wrapping the title in another Link produces nested anchors, which is invalid HTML and can cause confusing focus/hover behavior.

You can keep the hover style without nesting a second Link:

-      <h3 className="text-lg md:text-xl font-semibold text-ox-white mb-2 leading-tight">
-        <Link
-          href={`/dashboard/newsletter/${id}`}
-          className="hover:text-ox-purple-2"
-        >
-          {title}
-        </Link>
-      </h3>
+      <h3 className="text-lg md:text-xl font-semibold text-ox-white mb-2 leading-tight">
+        <span className="hover:text-ox-purple-2">{title}</span>
+      </h3>

🏁 Script executed:

#!/bin/bash
# Verify the actual file content and component signature
echo "=== Checking actual file content ==="
cat apps/web/src/components/newsletter/newsletter-card.tsx

echo -e "\n=== Checking component props type ==="
rg -n "interface|type.*Props|export.*function|const.*newsletter.*card" apps/web/src/components/newsletter/newsletter-card.tsx -A 5

Length of output: 1481


🏁 Script executed:

#!/bin/bash
# Search for usages of newsletter-card to understand how date is passed
echo "=== Searching for newsletter-card imports and usages ==="
rg -n "newsletter-card|NewsletterCard" apps/web/src --type=tsx --type=ts -B 2 -A 2 | head -100

Length of output: 84


🏁 Script executed:

#!/bin/bash
# Check if date is ever a Date object or always a string
echo "=== Checking date prop usage patterns in the file ==="
rg -n "date" apps/web/src/components/newsletter/newsletter-card.tsx -B 1 -A 1

Length of output: 296


🏁 Script executed:

#!/bin/bash
# Search for newsletter-card component usage with correct syntax
echo "=== Searching for NewsletterCard imports and usage ==="
rg -n "NewsletterCard|from.*newsletter-card" apps/web/src -C 3 | head -150

Length of output: 1708


🏁 Script executed:

#!/bin/bash
# Find where newsletter data is fetched/prepared to see date format
echo "=== Searching for newsletter data structures ==="
rg -n "newsletter.*date|date.*newsletter" apps/web/src -C 2 | head -150

Length of output: 1741


🏁 Script executed:

#!/bin/bash
# Check if there are any tests for this component
echo "=== Looking for newsletter-card tests ==="
fd -n "newsletter-card" apps/web --type f | xargs ls -la 2>/dev/null || echo "No test files found"

Length of output: 1776


Fix date parsing and avoid nested <Link> elements

Two issues here:

  1. Potential off‑by‑one dates with new Date("YYYY-MM-DD")
    new Date("2025-11-08") is interpreted as midnight UTC, so in many timezones the localized date will render as the previous day. Since your date prop is a string, it's safer to construct the date in local time:
-  const formattedDate = new Date(date).toLocaleDateString("en-US", {
+  const [year, month, day] = date.split("-").map(Number);
+  const formattedDate = new Date(year, month - 1, day).toLocaleDateString("en-US", {
     year: "numeric",
     month: "long",
     day: "numeric",
   });
  1. Nested Link components
    The outer Link already renders an <a>; wrapping the title in another Link produces nested anchors (lines 27–31), which is invalid HTML and can cause confusing focus/hover behavior.

Replace the inner Link with a span:

-      <h3 className="text-lg md:text-xl font-semibold text-ox-white mb-2 leading-tight">
-        <Link
-          href={`/dashboard/newsletter/${id}`}
-          className="hover:text-ox-purple-2"
-        >
-          {title}
-        </Link>
-      </h3>
+      <h3 className="text-lg md:text-xl font-semibold text-ox-white mb-2 leading-tight">
+        <span className="hover:text-ox-purple-2">{title}</span>
+      </h3>
📝 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 formattedDate = new Date(date).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
return (
<Link
href={`/dashboard/newsletter/${id}`}
className="border border-ox-gray hover:cursor-pointer w-full max-w-sm rounded-lg p-5 bg-ox-black-2 hover:border-ox-purple-2 transition-colors"
>
<div className="flex items-center gap-2 text-sm mb-3 text-ox-gray">
<Calendar className="w-4 h-4 text-ox-gray" />
<span>{formattedDate}</span>
</div>
<h3 className="text-lg md:text-xl font-semibold text-ox-white mb-2 leading-tight">
<Link
href={`/dashboard/newsletter/${id}`}
className="hover:text-ox-purple-2"
>
{title}
</Link>
</h3>
{excerpt && (
<p className="text-sm text-gray-300 leading-relaxed line-clamp-3">
{excerpt}
</p>
)}
</Link>
const [year, month, day] = date.split("-").map(Number);
const formattedDate = new Date(year, month - 1, day).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
return (
<Link
href={`/dashboard/newsletter/${id}`}
className="border border-ox-gray hover:cursor-pointer w-full max-w-sm rounded-lg p-5 bg-ox-black-2 hover:border-ox-purple-2 transition-colors"
>
<div className="flex items-center gap-2 text-sm mb-3 text-ox-gray">
<Calendar className="w-4 h-4 text-ox-gray" />
<span>{formattedDate}</span>
</div>
<h3 className="text-lg md:text-xl font-semibold text-ox-white mb-2 leading-tight">
<span className="hover:text-ox-purple-2">{title}</span>
</h3>
{excerpt && (
<p className="text-sm text-gray-300 leading-relaxed line-clamp-3">
{excerpt}
</p>
)}
</Link>
🤖 Prompt for AI Agents
In apps/web/src/components/newsletter/newsletter-card.tsx around lines 12 to 39,
the code uses new Date(date) which can parse "YYYY-MM-DD" as UTC (causing
off‑by‑one local dates) and also nests a Link inside another Link; to fix, parse
the incoming date string into local time by splitting the "YYYY-MM-DD" (or
otherwise extracting year, month, day) and constructing a Date with new
Date(year, monthIndex, day) so the displayed date is correct in all timezones,
and replace the inner Link used for the title with a non-anchor element (e.g., a
span or div with the hover text class) to avoid nested anchors while preserving
the styling.

Comment on lines +24 to +45
const grouped = groupByMonth(items);
const months = Object.keys(grouped).sort((a, b) => (a > b ? 1 : -1));

return (
<div className="space-y-8">
{months.map((m) => (
<section key={m}>
<h2 className="text-2xl text-ox-white font-semibold mb-4">{m}</h2>
<div className="flex gap-4">
{grouped[m].map((n) => (
<NewsletterCard
key={n.id}
id={n.id}
title={n.title}
date={n.date}
excerpt={n.excerpt}
/>
))}
</div>
</section>
))}
</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

🧩 Analysis chain

Month ordering currently alphabetical, not “newest first”

getAllNewslettersSorted() already returns items in date‑descending order, and groupByMonth preserves that insertion order when building the month map. Sorting Object.keys(grouped) with (a > b ? 1 : -1) reorders sections alphabetically by their label (e.g. “April 2025” before “February 2025”), which violates the “newest first” requirement for grouping.

You can rely on insertion order instead:

-  const grouped = groupByMonth(items);
-  const months = Object.keys(grouped).sort((a, b) => (a > b ? 1 : -1));
+  const grouped = groupByMonth(items);
+  const months = Object.keys(grouped);

If you ever need explicit date-based sorting independent of upstream order, consider returning an ordered array of [label, items] from groupByMonth using the underlying dates.


🏁 Script executed:

#!/bin/bash
# Find where groupByMonth is defined and getAllNewslettersSorted is implemented
rg -n "groupByMonth|getAllNewslettersSorted" --type=ts --type=tsx -A 5 -B 1 | head -100

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Check imports in the newsletter-list.tsx file
cat apps/web/src/components/newsletter/newsletter-list.tsx | head -30

Length of output: 898


🏁 Script executed:

#!/bin/bash
# Search for groupByMonth function definition more specifically
rg -n "^\s*(export\s+)?(function|const)\s+groupByMonth" --type=ts --type=tsx -A 15

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Find getAllNewslettersSorted implementation
rg -n "getAllNewslettersSorted" --type=ts --type=tsx -B 2 -A 10

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Find the lib/newsletters file
fd -type f -name "newsletters*" 

Length of output: 170


🏁 Script executed:

#!/bin/bash
# Search for groupByMonth without type constraints
rg -n "groupByMonth" -A 15 | head -80

Length of output: 4325


🏁 Script executed:

#!/bin/bash
# Look for where items are sorted before being passed to NewsletterList
rg -n "getAllNewslettersSorted\|NewsletterList" -B 2 -A 2 | head -60

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find where NewsletterList component is used
rg -n "NewsletterList" -B 3 -A 3 | head -100

Length of output: 1698


🏁 Script executed:

#!/bin/bash
# Search for where newsletter items are fetched or prepared
rg -n "newsletter" -l | head -20

Length of output: 462


🏁 Script executed:

#!/bin/bash
# Look for pages that might use NewsletterList
find . -name "*newsletter*" -type f | grep -E "\.(ts|tsx|js|jsx)$" | head -20

Length of output: 147


🏁 Script executed:

#!/bin/bash
# Check if there's a function that sorts newsletters by date
rg -n "sort.*newsletter\|newsletter.*sort" -i -B 2 -A 5 | head -100

Length of output: 0


🏁 Script executed:

#!/bin/bash
# View the full newsletter page to understand data flow
cat apps/web/src/app/(main)/dashboard/newsletter/page.tsx

Length of output: 144


🏁 Script executed:

#!/bin/bash
# Find getAllNewslettersSorted implementation
rg -n "getAllNewslettersSorted" -A 20 apps/web/src/lib/newsletters.ts

Length of output: 626


🏁 Script executed:

#!/bin/bash
# View the newsletter page with proper path handling
cat ./apps/web/src/app/\(main\)/dashboard/newsletter/page.tsx

Length of output: 3877


🏁 Script executed:

#!/bin/bash
# Alternative: use find to locate and display the file
fd "newsletter" "page.tsx" | head -1 | xargs cat

Length of output: 95


🏁 Script executed:

#!/bin/bash
# Search for where filtered is assigned in newsletter pages
rg -n "filtered\s*=" apps/web/src/app/ -B 5 -A 2

Length of output: 726


Remove alphabetical sort on month labels; insertion order preserves newest-first requirement

The data passed to NewsletterList is pre-sorted newest-first via getAllNewslettersSorted(), and groupByMonth preserves that insertion order when building the month map. However, sorting Object.keys(grouped) alphabetically by month label (e.g., "April 2025" before "February 2025") reorders the sections chronologically, violating the newest-first requirement.

Remove the sort to rely on insertion order:

-  const months = Object.keys(grouped).sort((a, b) => (a > b ? 1 : -1));
+  const months = Object.keys(grouped);
📝 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 grouped = groupByMonth(items);
const months = Object.keys(grouped).sort((a, b) => (a > b ? 1 : -1));
return (
<div className="space-y-8">
{months.map((m) => (
<section key={m}>
<h2 className="text-2xl text-ox-white font-semibold mb-4">{m}</h2>
<div className="flex gap-4">
{grouped[m].map((n) => (
<NewsletterCard
key={n.id}
id={n.id}
title={n.title}
date={n.date}
excerpt={n.excerpt}
/>
))}
</div>
</section>
))}
</div>
const grouped = groupByMonth(items);
const months = Object.keys(grouped);
return (
<div className="space-y-8">
{months.map((m) => (
<section key={m}>
<h2 className="text-2xl text-ox-white font-semibold mb-4">{m}</h2>
<div className="flex gap-4">
{grouped[m].map((n) => (
<NewsletterCard
key={n.id}
id={n.id}
title={n.title}
date={n.date}
excerpt={n.excerpt}
/>
))}
</div>
</section>
))}
</div>
🤖 Prompt for AI Agents
In apps/web/src/components/newsletter/newsletter-list.tsx around lines 24 to 45,
the code currently sorts Object.keys(grouped) alphabetically which reorders
month sections and breaks the newest-first insertion order; remove the
.sort(...) call so months is assigned directly from Object.keys(grouped) to
preserve insertion order from groupByMonth and thus retain newest-first display.

Comment on lines +269 to +285
export function getAllNewslettersSorted() {
return [...newsletters].sort((a, b) => (a.date < b.date ? 1 : -1));
}

export function groupByMonth(items: newsletter[]) {
const map: Record<string, newsletter[]> = {};
items.forEach((it) => {
const d = new Date(it.date);
if (isNaN(d.getTime())) {
console.warn(`Invalid date for newsletter fit.id}:`, it.date);
return;
}
const key = d.toLocaleString("default", { month: "long", year: "numeric" });
if (!map[key]) map[key] = [];
map[key].push(it);
});
return map;
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

Use a safe date parser for grouping and fix warning message

groupByMonth currently does:

const d = new Date(it.date);

Parsing "yyyy-mm-dd" this way uses UTC semantics, which can yield off‑by‑one dates and even the wrong month in some timezones when later formatted via toLocaleString. This can cause newsletters to appear under the wrong month header.

Given date is normalized to yyyy-mm-dd, it’s safer to construct the Date in local time, similar to the fix suggested for NewsletterCard:

-export function groupByMonth(items: newsletter[]) {
+function parseNewsletterDate(date: string) {
+  const [year, month, day] = date.split("-").map(Number);
+  return new Date(year, month - 1, day);
+}
+
+export function groupByMonth(items: newsletter[]) {
   const map: Record<string, newsletter[]> = {};
   items.forEach((it) => {
-    const d = new Date(it.date);
+    const d = parseNewsletterDate(it.date);
     if (isNaN(d.getTime())) {
-      console.warn(`Invalid date for newsletter fit.id}:`, it.date);
+      console.warn(`Invalid date for newsletter ${it.id}:`, it.date);
       return;
     }

You can also reuse parseNewsletterDate in places like NewsletterCard for consistency.


I'll verify the review comment by examining the relevant files and the code in question.


---

🏁 Script executed:

```shell
#!/bin/bash
# Verify the newsletters.ts file and the code at lines 269-285
if [ -f "apps/web/src/lib/newsletters.ts" ]; then
  echo "=== File exists. Showing lines 269-285 ==="
  sed -n '269,285p' "apps/web/src/lib/newsletters.ts"
  echo ""
  echo "=== Full context around groupByMonth function ==="
  grep -n "groupByMonth" "apps/web/src/lib/newsletters.ts" -A 20
else
  echo "File not found"
fi

Length of output: 1140


🏁 Script executed:

#!/bin/bash
# Search for newsletter type definition and date field format
echo "=== Searching for newsletter type definition ==="
grep -n "type newsletter\|interface newsletter" "apps/web/src/lib/newsletters.ts" -A 5
echo ""
echo "=== Searching for the static newsletters array ==="
grep -n "const newsletters" "apps/web/src/lib/newsletters.ts" -A 15

Length of output: 890


🏁 Script executed:

#!/bin/bash
# Search for NewsletterCard and similar date parsing issues
echo "=== Searching for NewsletterCard ==="
fd -e ts -e tsx | xargs grep -l "NewsletterCard" | head -5
echo ""
echo "=== Searching for other Date parsing with newsletter dates ==="
rg -n "new Date.*date" "apps/web/src/lib/newsletters.ts"

Length of output: 247


🏁 Script executed:

#!/bin/bash
# Check if there's a typo in console.warn - look for both fit.id and it.id patterns
echo "=== Checking exact console.warn line ==="
grep -n "console.warn.*Invalid date" "apps/web/src/lib/newsletters.ts"

Length of output: 113


🏁 Script executed:

#!/bin/bash
# Check entire newsletters.ts for date format in sample data
echo "=== Full content of newsletters.ts to understand data structure ==="
head -100 "apps/web/src/lib/newsletters.ts"

Length of output: 2800


🏁 Script executed:

#!/bin/bash
# Search for NewsletterCard component file to verify similar issue
echo "=== Finding NewsletterCard component ==="
find "apps/web" -name "*NewsletterCard*" -o -name "*newsletter*" -type f | grep -i card
echo ""
echo "=== Search for NewsletterCard usage or definition ==="
rg -n "NewsletterCard" "apps/web/src" | head -20

Length of output: 479


🏁 Script executed:

#!/bin/bash
# Verify the complete groupByMonth function and surrounding code
echo "=== Complete groupByMonth function and surrounding context ==="
rg -n -A 25 "export function groupByMonth" "apps/web/src/lib/newsletters.ts"

Length of output: 556


🏁 Script executed:

#!/bin/bash
# Check for getAllNewslettersSorted to understand date comparison
echo "=== getAllNewslettersSorted function ==="
rg -n -B 2 -A 5 "getAllNewslettersSorted" "apps/web/src/lib/newsletters.ts"

Length of output: 292


🏁 Script executed:

#!/bin/bash
# Look for toLocaleString usage to understand the month grouping logic
echo "=== toLocaleString usage in groupByMonth ==="
rg -n "toLocaleString" "apps/web/src/lib/newsletters.ts" -B 3 -A 3

Length of output: 305


🏁 Script executed:

#!/bin/bash
# Check if parseNewsletterDate or similar utility already exists
echo "=== Checking for existing date parsing utilities ==="
rg -n "parseNewsletter|parseDateString|dateParser" "apps/web/src/lib/newsletters.ts"

Length of output: 52


🏁 Script executed:

#!/bin/bash
# Get full context of the static newsletters array definition
echo "=== Finding and displaying newsletters data ==="
rg -n "newsletters.*=.*\[" "apps/web/src/lib/newsletters.ts" -A 40

Length of output: 1474


🏁 Script executed:

#!/bin/bash
# Verify the exact line with console.warn to confirm typo claim
echo "=== Exact content with visible characters ==="
sed -n '280,281p' "apps/web/src/lib/newsletters.ts" | cat -A

Length of output: 134


🏁 Script executed:

#!/bin/bash
# Check if there are any other groupByMonth or date-related functions
echo "=== All functions in newsletters.ts ==="
rg -n "^export function|^function" "apps/web/src/lib/newsletters.ts"

Length of output: 195


🏁 Script executed:

#!/bin/bash
# Verify NewsletterCard file and check for date parsing there
echo "=== Find NewsletterCard file ==="
find "apps/web/src" -type f \( -name "*Card*" -o -name "*newsletter*" \) | xargs grep -l "NewsletterCard" 2>/dev/null | head -3
echo ""
echo "=== If found, check its content for date parsing ==="
fd "NewsletterCard" "apps/web/src" && cat "apps/web/src/components/NewsletterCard.tsx" | head -50

Length of output: 271


🏁 Script executed:

#!/bin/bash
# Double check: look for the exact typo mentioned in the review
echo "=== Searching for 'fit.id' pattern ==="
rg "fit\.id\|fit\[id\]" "apps/web/src/lib/newsletters.ts"

Length of output: 38


🏁 Script executed:

#!/bin/bash
# Examine the entire newsletters.ts to get full picture
echo "=== Word count and general structure ==="
wc -l "apps/web/src/lib/newsletters.ts"
echo ""
echo "=== Entire file content ==="
cat "apps/web/src/lib/newsletters.ts"

Length of output: 7122


I need to check the NewsletterCard component to verify if there's a similar date parsing issue mentioned in the review.


---

🏁 Script executed:

```shell
#!/bin/bash
# Check the newsletter-card.tsx file for date parsing
echo "=== Content of newsletter-card.tsx ==="
cat "apps/web/src/components/newsletter/newsletter-card.tsx"

Length of output: 1213


🏁 Script executed:

#!/bin/bash
# Search for any date parsing or new Date calls in the component
echo "=== Searching for date-related code in newsletter components ==="
rg -n "new Date\|toLocaleString\|date" "apps/web/src/components/newsletter/" -B 2 -A 2

Length of output: 64


🏁 Script executed:

#!/bin/bash
# Verify the full getAllNewslettersSorted function and how it's used
echo "=== Searching for getAllNewslettersSorted usage ==="
rg -rn "getAllNewslettersSorted" "apps/web/src"

Length of output: 273


Fix timezone-aware date parsing and correct string template typo

Two related issues found:

  1. Line 278 — String template typo: fit.id} should be ${it.id}. This breaks the warning message rendering.

  2. Line 276 — Timezone parsing issue: new Date(it.date) parses yyyy-mm-dd as UTC per ISO 8601. When later converted via toLocaleString, this can shift to the wrong date in timezones with negative UTC offsets (e.g., UTC-5), causing newsletters to appear under incorrect month headers.

The same timezone issue also exists in apps/web/src/components/newsletter/newsletter-card.tsx (line 12), where new Date(date).toLocaleDateString() is used.

Use local-time date construction instead:

-export function groupByMonth(items: newsletter[]) {
+function parseNewsletterDate(date: string) {
+  const [year, month, day] = date.split("-").map(Number);
+  return new Date(year, month - 1, day);
+}
+
+export function groupByMonth(items: newsletter[]) {
   const map: Record<string, newsletter[]> = {};
   items.forEach((it) => {
-    const d = new Date(it.date);
+    const d = parseNewsletterDate(it.date);
     if (isNaN(d.getTime())) {
-      console.warn(`Invalid date for newsletter fit.id}:`, it.date);
+      console.warn(`Invalid date for newsletter ${it.id}:`, it.date);
       return;
     }

Apply the same fix in newsletter-card.tsx line 12 for consistency:

-const formattedDate = new Date(date).toLocaleDateString(...)
+const formattedDate = parseNewsletterDate(date).toLocaleDateString(...)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant