Skip to content

Conversation

@Nachiket-Roy
Copy link

@Nachiket-Roy Nachiket-Roy commented Nov 15, 2025

closes #155
Demo video :-

15.11.2025_17.11.19_REC.1.mp4

Mobile view :-
Screenshot 2025-11-15 172912

Key Changes :
All newsletter entries now live in /src/data/newsletter.ts
Adding future newsletters only requires appending to this file
Ensures clean separation of data from UI logic
Implemented sorting and filtering
Newsletters load sorted by latest date by default
Users can filter by: Month, Year
Dates must follow the format: Dec 10, 2025
(The sorting logic relies on this exact format.)
Smooth fade-in animation when cards enter the viewport
Circular glowing dot animation to indicate active card on scroll
Cards alternate left/right layout on larger screens
Fully responsive for mobile and desktop
Clicking a newsletter card opens a modal with: Full article content

Summary by CodeRabbit

  • New Features
    • Added a Newsletter section with an interactive vertical timeline and animated progress indicator.
    • Timeline cards show image, title, date, author and preview with alternating left/right layout.
    • Filtering controls for month/year and sorting by newest/oldest.
    • Expanded full-article view (modal-like) with keyboard/focus support and easy close actions.
    • Newsletter link added to the dashboard sidebar.
    • Initial set of sample newsletters included.

@vercel
Copy link

vercel bot commented Nov 15, 2025

@Nachiket-Roy 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 15, 2025

Warning

Rate limit exceeded

@Nachiket-Roy has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 6 minutes and 56 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 5c579b3 and 2eee502.

📒 Files selected for processing (1)
  • apps/web/src/app/(main)/dashboard/newsletter/page.tsx (1 hunks)

Walkthrough

Adds a timeline-based newsletter page component with filtering/sorting and an accessible full-article modal, registers a sidebar navigation entry for "Newsletter", and introduces a newsletter data module exporting a Newsletter type and sample entries.

Changes

Cohort / File(s) Change Summary
Newsletter Timeline UI Component
apps/web/src/app/(main)/dashboard/newsletter/page.tsx
New client-side React/Next.js component rendering a vertical timeline of newsletter cards with alternating left/right layout, IntersectionObserver-driven visibility tracking, scroll progress indicator, interactive dots/connectors, month/year filtering and sort order, focus-trapped "Read Full Article" modal, and related state/refs/cleanup.
Sidebar Navigation
apps/web/src/components/dashboard/Sidebar.tsx
Added a Newsletter navigation entry linking to /dashboard/newsletter and imported NewspaperIcon.
Newsletter Data
apps/web/src/data/newsletter.ts
New module exporting a Newsletter interface and newsletters: Newsletter[] containing 9 sample newsletter objects (id, title, date, author, preview, content, image, optional takeaways).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Page as Newsletter Page
    participant Observer as IntersectionObserver
    participant DOM as DOM/Refs

    User->>Page: Open /dashboard/newsletter
    activate Page
    Page->>DOM: mount, init refs, compute layout
    Page->>Observer: observe newsletter card elements
    deactivate Page

    User->>Page: Scroll timeline
    activate Page
    Page->>Page: compute activeProgress
    Page->>DOM: update central fill & active dot
    deactivate Page

    Observer->>Page: card enters viewport
    activate Observer
    Observer->>Page: mark visible, trigger connector animation
    Page->>DOM: reveal card, update glowing/active state
    deactivate Observer

    User->>Page: Apply filter/sort
    activate Page
    Page->>Page: update filterMonth/filterYear/sortType
    Page->>DOM: re-render filtered list
    deactivate Page

    User->>Page: Click "Read Full Article"
    activate Page
    Page->>DOM: open modal (selectedNewsletter), trap focus
    User->>Page: Click outside or press Esc
    Page->>Page: clear selection, restore focus, close modal
    deactivate Page
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Pay special attention to:
    • IntersectionObserver setup, thresholds, and cleanup in page.tsx
    • activeProgress and lineHeight calculations tied to layout/resizing
    • Modal focus trap, Escape handling, and previous-focus restoration
    • Date parsing/ordering in apps/web/src/data/newsletter.ts
    • Accessibility attributes and keyboard interactions

Possibly related issues

  • #155: [bounty-to-hire]: create ui for newsletter — Implements the newsletter page, date organization, sample data and readable article view requested by the issue.
  • apsinghdev/opensox#122 — Adds the /dashboard/newsletter route, page UI, and newsletter dataset consistent with that issue's work.

Poem

🐰 i hopped to code a tiny news parade,

cards aligned where glowing dots were made,
months and years sorted neat with care,
a modal blooms to show the story there,
nibble headlines — celebrate the share! 🎉📰

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Added new page Newsletter' clearly and concisely describes the main change - adding a new newsletter page component.
Linked Issues check ✅ Passed PR meets core requirements: newsletter page with date organization, rich content support (text, links, images), code-based content management, complete newsletter listing ordered by date, and readability features including modal view.
Out of Scope Changes check ✅ Passed All changes are in-scope: newsletter page implementation, data structure, sidebar navigation integration. No unrelated modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

❤️ Share

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

@cla-assistant
Copy link

cla-assistant bot commented Nov 15, 2025

CLA assistant check
All committers have signed the CLA.

@cla-assistant
Copy link

cla-assistant bot commented Nov 15, 2025

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (4)
apps/web/src/data/newsletter.ts (1)

8-8: Fix formatting inconsistency.

Remove the space before the colon for consistency with TypeScript conventions.

Apply this diff:

-  takeaways? : string[];
+  takeaways?: string[];
apps/web/src/app/(main)/dashboard/newsletter/page.tsx (3)

269-278: Use Next.js Image component for optimization.

Replace the native <img> tag with Next.js Image component for automatic optimization, lazy loading, and better performance.

+"use client";
 import React, { useState, useEffect, useRef } from "react";
+import Image from "next/image";
 import { Calendar, User, ArrowRight, ArrowLeft } from "lucide-react";

Then update the image rendering:

                       {newsletter.image && (
                         <div className="relative w-full h-48 overflow-hidden">
-                          <img
+                          <Image
                             src={newsletter.image}
                             alt={newsletter.title}
+                            fill
+                            sizes="(max-width: 768px) 100vw, 50vw"
                             className="w-full h-full object-cover transition-transform duration-500 hover:scale-110"
                           />

Note: You'll need to configure the Unsplash domain in next.config.ts under images.remotePatterns.


362-371: Use Next.js Image component in modal.

Similar to the card images, the modal hero image should also use the Next.js Image component for optimization.

                 {/* Hero Image */}
                 {selectedNewsletter.image && (
                   <div className="relative w-full h-96 overflow-hidden">
-                    <img
+                    <Image
                       src={selectedNewsletter.image}
                       alt={selectedNewsletter.title}
+                      fill
+                      sizes="(max-width: 1024px) 100vw, 1024px"
+                      priority
                       className="w-full h-full object-cover"
                     />

244-267: Consider extracting inline style calculation.

The complex conditional styling logic for card expansion could be extracted into a helper function for improved readability and maintainability.

const getCardStyle = (
  index: number,
  expandingCard: number | null,
  cardRefs: React.MutableRefObject<(HTMLDivElement | null)[]>
) => {
  if (expandingCard !== index) return {};
  
  const rect = cardRefs.current[index]?.getBoundingClientRect();
  if (!rect) return {};
  
  return {
    top: rect.top,
    left: rect.left,
    width: cardRefs.current[index]?.offsetWidth,
    height: cardRefs.current[index]?.offsetHeight,
    transition: "all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1)",
  };
};

// Then in JSX:
style={{
  transitionDelay: isVisible ? "0ms" : `${index * 120}ms`,
  ...getCardStyle(index, expandingCard, cardRefs),
}}
📜 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 942bf46.

📒 Files selected for processing (3)
  • apps/web/src/app/(main)/dashboard/newsletter/page.tsx (1 hunks)
  • apps/web/src/components/dashboard/Sidebar.tsx (2 hunks)
  • apps/web/src/data/newsletter.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/src/app/(main)/dashboard/newsletter/page.tsx (1)
apps/web/src/data/newsletter.ts (2)
  • Newsletter (1-10)
  • newsletters (11-147)
🔇 Additional comments (2)
apps/web/src/components/dashboard/Sidebar.tsx (1)

39-43: LGTM!

The newsletter route is properly integrated into the sidebar navigation, following the existing pattern for route definitions.

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

336-336: animate-in class is properly defined—no action needed.

The animate-in utility class is provided by the tailwindcss-animate plugin, which is correctly installed as a dependency in apps/web/package.json (version ^1.0.7) and configured in apps/web/tailwind.config.ts. The class will work as intended.

Likely an incorrect or invalid review comment.

@Nachiket-Roy Nachiket-Roy marked this pull request as draft November 15, 2025 12:28
@Nachiket-Roy Nachiket-Roy marked this pull request as ready for review November 15, 2025 12:50
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 (1)
apps/web/src/app/(main)/dashboard/newsletter/page.tsx (1)

372-457: Improve modal focus management for full accessibility (focus trap / initial focus).

The modal now has ESC handling and ARIA attributes, which is great. However, focus isn’t moved into the dialog or trapped while it’s open, so keyboard users can still tab to background content and don’t get an obvious starting point inside the modal.

Consider:

  • Moving focus to the “Back to Timeline” button (or a dedicated close button) when selectedNewsletter becomes non‑null.
  • Trapping focus within the modal and restoring focus to the triggering element on close (e.g., via a small custom hook or a library like react-focus-lock).
🧹 Nitpick comments (3)
apps/web/src/data/newsletter.ts (1)

1-10: Document the required date string format to avoid subtle breakage.

The timeline’s parseDate helper assumes dates like "Dec 10, 2025" (3‑letter month, MMM DD, YYYY). If someone later adds data with a different format, sorting/filtering will silently misbehave.

Consider documenting this invariants directly on the type:

 export interface Newsletter {
   id: number;
   title: string;
-  date: string;
+  /**
+   * Display date in format "MMM DD, YYYY" (e.g. "Dec 10, 2025").
+   * Timeline sorting/filtering logic relies on this exact format.
+   */
+  date: string;
   author: string;
   preview: string;
   content: string;
   takeaways?: string[];
   image: string;
 }
apps/web/src/app/(main)/dashboard/newsletter/page.tsx (2)

64-73: Re-run the initial animation when filters or sort change to keep indices in sync.

visibleCards and activeDot are keyed by the map index, but the init effect only runs once. After changing filterMonth, filterYear, or sortType, the order/contents of filteredNewsletters can change while the stored indices still refer to the old ordering, so some cards may render as “already visible” or mismatched.

You can cheaply realign things by re-running the init effect whenever filters or sort change:

-  useEffect(() => {
+  useEffect(() => {
     const timer = setTimeout(() => {
       setLineHeight(100);
       setActiveDot(0);
       setVisibleCards(new Set([0]));
       setGlowingDot(0);
       setTimeout(() => setGlowingDot(null), 300);
     }, 100);
     return () => clearTimeout(timer);
-  }, []);
+  }, [filterMonth, filterYear, sortType]);

This keeps the “first card is active and visible” behavior consistent with the current filter/sort order.


218-363: Add an empty‑state message when no newsletters match the filters.

If the user selects a year/month combination with no data (e.g., 2024/2023), the timeline area goes blank without explanation. A small empty state will make this much clearer.

For example, just after the filteredNewsletters.map(...) block:

-        <div className="relative space-y-8 md:space-y-24 pt-8">
-          {filteredNewsletters.map((newsletter, index) => {
+        <div className="relative space-y-8 md:space-y-24 pt-8">
+          {filteredNewsletters.map((newsletter, index) => {
             // ...
-          })}
+          })}
+
+          {filteredNewsletters.length === 0 && (
+            <div className="pt-8 text-center text-slate-400">
+              No newsletters found for the selected filters.
+            </div>
+          )}
         </div>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 942bf46 and e7a2e7a.

📒 Files selected for processing (2)
  • apps/web/src/app/(main)/dashboard/newsletter/page.tsx (1 hunks)
  • apps/web/src/data/newsletter.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/src/app/(main)/dashboard/newsletter/page.tsx (1)
apps/web/src/data/newsletter.ts (2)
  • Newsletter (1-10)
  • newsletters (11-147)

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 (2)
apps/web/src/app/(main)/dashboard/newsletter/page.tsx (2)

208-215: Replace unsafe type assertion.

The type assertion as "newest" | "oldest" on line 210 is unsafe and bypasses TypeScript's type checking. Although the select options are controlled, explicit typing is preferred.

Apply this diff:

         <select
           value={sortType}
-          onChange={(e) => setSortType(e.target.value as "newest" | "oldest")}
+          onChange={(e) => setSortType(e.target.value as typeof sortType)}
           className="bg-[#15161A] border border-[#27272A] rounded-lg px-3 py-2"
         >

Or use a more explicit handler:

+        onChange={(e) => {
+          const value = e.target.value;
+          if (value === "newest" || value === "oldest") {
+            setSortType(value);
+          }
+        }}

122-154: Fix IntersectionObserver to re-initialize when filtered newsletters change.

The effect has an empty dependency array, so the observer is created only once on mount. When filteredNewsletters changes (via filter/sort changes), React renders a different set of cards and updates cardRefs.current via ref callbacks, but the observer never re-runs to observe the new cards. This causes newly filtered cards to not be tracked for visibility and animations.

Apply this diff to re-initialize the observer when the filtered list changes:

     return () => observer.disconnect();
-  }, []);
+  }, [filteredNewsletters]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e7a2e7a and 0a38711.

📒 Files selected for processing (1)
  • apps/web/src/app/(main)/dashboard/newsletter/page.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/src/app/(main)/dashboard/newsletter/page.tsx (1)
apps/web/src/data/newsletter.ts (2)
  • Newsletter (1-10)
  • newsletters (11-147)
🔇 Additional comments (3)
apps/web/src/app/(main)/dashboard/newsletter/page.tsx (3)

29-52: LGTM: Deterministic date parsing implemented.

The explicit parseDate function with month name mapping correctly addresses the previous concern about browser-dependent date parsing. This ensures reliable date handling across all environments.


83-119: LGTM: Focus trap implemented correctly.

The focus management handles both Tab and Shift+Tab to cycle focus within the modal, stores the previous active element for restoration, and properly cleans up event listeners. This provides good keyboard accessibility for the modal.


419-509: LGTM: Modal accessibility features properly implemented.

The modal includes proper ARIA attributes (role="dialog", aria-modal="true", aria-labelledby), has a focusable back button, and integrates with the focus trap and keyboard handlers. This addresses the previous accessibility review comments.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

122-154: Recreate observer when filtered newsletters change.

The IntersectionObserver has an empty dependency array, so it's never recreated when the newsletter list changes due to filtering or sorting. This causes the observer to watch stale DOM references, breaking the visibility/animation system when filters are applied.

Apply this diff:

     return () => observer.disconnect();
-  }, []);
+  }, [filteredNewsletters.length]);

This ensures the observer is recreated whenever the filtered list changes, while avoiding unnecessary recreation when only the list order changes (since .length is stable for reordered arrays).

🧹 Nitpick comments (4)
apps/web/src/app/(main)/dashboard/newsletter/page.tsx (4)

54-70: Consider memoizing the filtered newsletters.

The filter and sort operations run on every render. While the current dataset is small, wrapping this in useMemo would prevent unnecessary recomputation and follow React best practices.

Apply this diff:

+  const filteredNewsletters = React.useMemo(() => {
+    return newsletters
-  const filteredNewsletters = newsletters
       .filter((n) => {
         const d = parseDate(n.date);
         const month = d.getMonth() + 1;
         const year = d.getFullYear();
 
         const monthMatches =
           filterMonth === "all" || Number(filterMonth) === month;
         const yearMatches = filterYear === "all" || Number(filterYear) === year;
 
         return monthMatches && yearMatches;
       })
       .sort((a, b) => {
         const da = parseDate(a.date).getTime();
         const db = parseDate(b.date).getTime();
         return sortType === "newest" ? db - da : da - db;
       });
+  }, [filterMonth, filterYear, sortType]);

83-119: Consider guarding against empty focusable elements.

The focus trap logic assumes there are focusable elements in the modal. While unlikely given the modal structure, adding a guard would make the code more robust.

Add a check after line 102:

     const focusable = modal.querySelectorAll<HTMLElement>(
       'button, a, input, textarea, select, [tabindex]:not([tabindex="-1"])'
     );
+    if (focusable.length === 0) return;
+    
     const first = focusable[0];
     const last = focusable[focusable.length - 1];

236-245: Derive year options from newsletter data.

The year filter has hardcoded values (2025, 2024, 2023) that will become outdated as new newsletters are added. Deriving available years from the newsletter data ensures the filter stays current without code changes.

Add this logic before the return statement (around line 193):

  const availableYears = React.useMemo(() => {
    const years = new Set(
      newsletters.map((n) => parseDate(n.date).getFullYear())
    );
    return Array.from(years).sort((a, b) => b - a);
  }, []);

Then replace the hardcoded options:

         <select
           value={filterYear}
           onChange={(e) => setFilterYear(e.target.value)}
           className="bg-[#15161A] border border-[#27272A] rounded-lg px-3 py-2"
         >
           <option value="all">All Years</option>
-          <option value="2025">2025</option>
-          <option value="2024">2024</option>
-          <option value="2023">2023</option>
+          {availableYears.map((year) => (
+            <option key={year} value={year}>
+              {year}
+            </option>
+          ))}
         </select>

329-352: Consider moving animation calculations to the button click handler.

The inline style at lines 341-350 calls getBoundingClientRect() during render, which can be fragile if refs aren't ready. Consider calculating the animation start position in the button's onClick handler and storing it in state for more reliable animation behavior.

Example approach:

const [expandingCardPosition, setExpandingCardPosition] = useState<{
  top: number;
  left: number;
  width: number;
  height: number;
} | null>(null);

// In the button onClick:
onClick={() => {
  const rect = cardRefs.current[index]?.getBoundingClientRect();
  if (rect) {
    setExpandingCardPosition({
      top: rect.top,
      left: rect.left,
      width: rect.width,
      height: rect.height,
    });
  }
  setExpandingCard(index);
  setTimeout(() => {
    setSelectedNewsletter(newsletter);
  }, 600);
}}

// In the style prop:
...(expandingCard === index && expandingCardPosition && {
  ...expandingCardPosition,
  transition: "all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1)",
})
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a38711 and 5c579b3.

📒 Files selected for processing (1)
  • apps/web/src/app/(main)/dashboard/newsletter/page.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/src/app/(main)/dashboard/newsletter/page.tsx (1)
apps/web/src/data/newsletter.ts (2)
  • Newsletter (1-10)
  • newsletters (11-147)
🔇 Additional comments (3)
apps/web/src/app/(main)/dashboard/newsletter/page.tsx (3)

29-52: LGTM! Date parsing is now deterministic.

The explicit parseDate function addresses the previous concern about browser-dependent date parsing. The implementation correctly maps month abbreviations to indices and constructs a Date object deterministically.


180-191: LGTM! Escape key handler properly uses closeModal().

The handler correctly calls closeModal() to ensure focus restoration, addressing the previous review feedback.


418-425: LGTM! Modal accessibility attributes properly implemented.

The modal includes proper ARIA attributes (role="dialog", aria-modal="true", aria-labelledby) that address the previous accessibility concerns.

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