-
Notifications
You must be signed in to change notification settings - Fork 116
Implement Newsletter dashboard #160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
@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. |
WalkthroughImplements 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
Sequence DiagramssequenceDiagram
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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes
Possibly related issues
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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 issuesTwo 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 duplicategroupByMonthimplementationThe commented local
groupByMonthimplementation 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‑insensitiveTwo small improvements to the search/filter logic:
useMemois ineffective right now
const items = getAllNewslettersSorted();creates a new array each render, soitemsalways changes identity and theuseMemorecomputes every time anyway. Either:
- Hoist the call into a
useMemowith an empty dep array, or- Compute
itemsonce outside the component, or- Drop
useMemoonfilteredand just compute directly (the dataset is small).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 linksTwo related but independent points:
- Final blockquote edge case
blockquoteBufferis 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 inblockquoteBufferand 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;
- Premium gating of article deep links (verify requirement)
The listing page (/dashboard/newsletter) explicitly gates access withuseSubscription, but this detail page renders anygetNewsletterById(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
/dashboardalready 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis 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 goodThe new
Newspapericon import and/dashboard/newsletterroute 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‑caseThe added
.prosestyles (links, inline code, paragraph spacing, heading colors) are cohesive with the newsletter article layout and should improve readability without clashing with Tailwind Typography defaults.
| 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; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Tighten embed handling to truly skip invalid URLs
In the lang === "embed" branch:
- You correctly validate protocol for successfully parsed URLs and
returnearly when it’s nothttp/https. - In the
catchblock, you logconsole.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.
| 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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Fix date parsing and avoid nested <Link> elements
Two issues here:
- 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 yourdatefield is a plainyyyy-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",
});- Nested
Linkcomponents
The outerLinkalready renders an<a>; wrapping the title in anotherLinkproduces 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 5Length 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 -100Length 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 1Length 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 -150Length 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 -150Length 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:
- 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 yourdateprop 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",
});- Nested
Linkcomponents
The outerLinkalready renders an<a>; wrapping the title in anotherLinkproduces 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.
| 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.
| 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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -100Length 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 -30Length 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 15Length of output: 27
🏁 Script executed:
#!/bin/bash
# Find getAllNewslettersSorted implementation
rg -n "getAllNewslettersSorted" --type=ts --type=tsx -B 2 -A 10Length 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 -80Length 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 -60Length of output: 0
🏁 Script executed:
#!/bin/bash
# Find where NewsletterList component is used
rg -n "NewsletterList" -B 3 -A 3 | head -100Length of output: 1698
🏁 Script executed:
#!/bin/bash
# Search for where newsletter items are fetched or prepared
rg -n "newsletter" -l | head -20Length 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 -20Length 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 -100Length 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.tsxLength of output: 144
🏁 Script executed:
#!/bin/bash
# Find getAllNewslettersSorted implementation
rg -n "getAllNewslettersSorted" -A 20 apps/web/src/lib/newsletters.tsLength of output: 626
🏁 Script executed:
#!/bin/bash
# View the newsletter page with proper path handling
cat ./apps/web/src/app/\(main\)/dashboard/newsletter/page.tsxLength of output: 3877
🏁 Script executed:
#!/bin/bash
# Alternative: use find to locate and display the file
fd "newsletter" "page.tsx" | head -1 | xargs catLength 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 2Length 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.
| 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.
| 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 15Length 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 -20Length 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 3Length 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 40Length 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 -ALength 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 -50Length 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 2Length 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:
-
Line 278 — String template typo:
fit.id}should be${it.id}. This breaks the warning message rendering. -
Line 276 — Timezone parsing issue:
new Date(it.date)parsesyyyy-mm-ddas UTC per ISO 8601. When later converted viatoLocaleString, 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(...)
fix: #155
web/src/lib/newsletter.tsScreen.Recording.2025-11-14.at.5.40.22.PM.mov
Summary by CodeRabbit
Release Notes