Skip to content

Conversation

@Shivam107
Copy link

@Shivam107 Shivam107 commented Oct 23, 2025

Fixes : #122

demoNewsletter1.mp4

any changes should i make on this?

Summary by CodeRabbit

  • New Features

    • Browse, search and view newsletter issues with rich sections (text, image, video) and per-issue detail pages.
    • Share newsletters via native share or clipboard; share UI on cards and detail pages.
  • UI/UX Updates

    • Newsletter added to dashboard navigation.
    • Fixed sidebar behavior, scrollable content areas, layout and visual tweaks (including color token update and scrollbar gutter).
  • Chores

    • Backend API now exposes newsletter endpoints; remote image host and media player dependency added.

@vercel
Copy link

vercel bot commented Oct 23, 2025

@Shivam107 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

coderabbitai bot commented Oct 23, 2025

Walkthrough

Adds an end-to-end newsletter feature: new API router and service (list, bySlug), frontend pages/components/hooks with mock data, media rendering via react-player, Next image pattern and TypeScript declaration updates, plus layout and styling tweaks.

Changes

Cohort / File(s) Summary
API: Router & Service
apps/api/src/routers/_app.ts, apps/api/src/routers/newsletter.ts, apps/api/src/services/newsletter.service.ts
Replace payment with newsletter in appRouter; add newsletterRouter with list and bySlug procedures; add newsletterService with list(search?: string) and bySlug(slug: string) using Prisma.
Frontend: Pages & Client Hooks
apps/web/src/app/(main)/dashboard/newsletters/page.tsx, apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx, apps/web/src/app/(main)/dashboard/newsletters/[slug]/NewsletterDetailClient.tsx, apps/web/src/app/api/newsletter.ts
Add newsletter index and detail routes; server-side detail renders client component with fallback; add useNewsletterList (mock fallback) and useNewsletterDetail (trpc.bySlug).
Frontend: Components & Data
apps/web/src/components/newsletter/*, apps/web/src/components/dashboard/Sidebar.tsx, apps/web/src/data/newsletters.ts
Add components: NewsletterCard, NewsletterHero, NewsletterHeader, NewsletterSectionRenderer, EngagementBar; add newsletterIssues mock data; add newsletter route to Sidebar and switch active detection to prefix matching.
Media Player & Types
apps/web/package.json, apps/web/src/types/next-auth.d.ts
Add dependency react-player and module declaration declare module "react-player/lazy" to support dynamic import of the player.
Layout & Styling
apps/web/src/app/(main)/dashboard/layout.tsx, apps/web/src/app/SessionWrapper.tsx, apps/web/src/globals.css, apps/web/tailwind.config.ts
Dashboard overflow/scroll adjustments, SessionWrapper formatting, add scrollbar-gutter: stable, and change ox-white token to #ffffff in Tailwind theme.
Config: Next image patterns
apps/web/next.config.js
Add remote image pattern { protocol: "https", hostname: "assets.opensox.dev" }.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant IndexPage as Newsletter Index
    participant DetailPage as Newsletter Detail (server)
    participant Client as NewsletterDetailClient
    participant Hooks as Client Hooks
    participant TRPC as tRPC Client
    participant API as Newsletter Router
    participant Service as Newsletter Service
    participant DB as Prisma

    User->>IndexPage: open /dashboard/newsletters
    IndexPage->>Hooks: useNewsletterList(search?)
    Hooks-->>IndexPage: return mock data or live result

    User->>DetailPage: open /dashboard/newsletters/[slug]
    DetailPage->>Client: render with slug + fallback
    Client->>Hooks: useNewsletterDetail(slug)
    Hooks->>TRPC: newsletter.bySlug.query({slug})
    TRPC->>API: bySlug({slug})
    API->>Service: bySlug(slug)
    Service->>DB: fetch issue + sections
    DB-->>Service: issue data
    Service-->>API: issue
    API-->>TRPC: issue
    TRPC-->>Hooks: issue
    Hooks-->>Client: issue data
    Client->>Client: render Hero + Sections (lazy ReactPlayer)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Potential attention points:

  • Prisma queries and selected fields in newsletter.service.ts.
  • Dynamic import and SSR behavior for react-player in NewsletterSectionRenderer.
  • Type declarations (react-player/lazy) and their alignment with bundler/TS config.
  • Interaction between mock hooks and trpc hooks (consistency of shapes).

Possibly related PRs

  • auto onboarding #131 — Conflicts with this PR: that PR adds payment: paymentRouter while this change replaces it with newsletter: newsletterRouter.

Poem

🐰 I hopped through code, with a ribbon and pen,

New issues and heroes in rows once again.
I stitched up the API, laid mock data neat,
A player that hums and a page you can greet.
Share, copy, and read — newsletter treats! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'built a newsletter section' partially aligns with the changeset. While a newsletter feature is added, the PR encompasses much more than just 'a section'—it includes a complete newsletter surface with routers, services, UI components, data models, and dashboard integration. Consider a more specific title that captures the full scope, such as 'Add newsletter feature with API router, service, and UI components' or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

Copy link

@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: 12

🧹 Nitpick comments (19)
apps/web/src/app/(main)/dashboard/newsletters/engagementBar.tsx (1)

15-39: Good implementation with proper fallback handling.

The share functionality correctly tries the native Share API before falling back to clipboard copy. The use of window.location.origin ensures the shared URL points to the current deployment.

Consider showing user feedback when sharing/copying fails (lines 27, 37), not just logging to console. A toast notification or error message would improve the user experience.

apps/web/src/types/next-auth.d.ts (1)

14-18: Module declaration works but consider file organization.

The type declaration for react-player/lazy is technically correct and will resolve TypeScript errors. However, this file is named next-auth.d.ts but now contains declarations unrelated to NextAuth.

Consider moving react-player declarations to a separate type definition file (e.g., react-player.d.ts) for better code organization.

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

13-22: Consider explicit locale for date formatting.

The date formatting uses undefined as the locale, which defaults to the user's browser locale. This is acceptable but could lead to inconsistent formatting across users. Consider using a specific locale (e.g., "en-US") or making it configurable based on user preferences.

apps/api/src/routers/newsletter.ts (1)

5-13: API router implementation is correct.

The newsletter router properly defines two procedures with appropriate input validation and service integration.

The double .optional() on line 7 (z.object({ search: z.string().optional() }).optional()) is valid but could be simplified to z.object({ search: z.string().optional() }) if you always want to require passing an object (even if empty). The current pattern allows calling without any input at all, which works but is unconventional.

If you prefer to simplify:

  list: publicProcedure
-    .input(z.object({ search: z.string().optional() }).optional())
+    .input(z.object({ search: z.string().optional() }))
    .query(async ({ input }) => newsletterService.list(input?.search)),

Then change the usage to input.search instead of input?.search since input will always be an object.

apps/web/src/components/newsletter/NewsletterCard.tsx (3)

52-53: Fix useCallback deps (remove unused).

Only sharePath is used; extra deps cause needless re-renders.

-    [issue.summary, issue.title, sharePath]
+    [sharePath]

86-115: Remove unused MediaPreview or render it.

Dead code hurts maintainability. If you don’t plan to show media here, delete it; otherwise, wire it into the card.

Option A — remove:

- function MediaPreview({ issue }: NewsletterCardProps) {
-   if (!issue.heroMediaUrl) {
-     return null;
-   }
-   if (issue.heroMediaType === "video") {
-     return (
-       <div className="relative w-full overflow-hidden rounded-xl border border-ox-gray md:w-56">
-         <video
-           src={issue.heroMediaUrl}
-           controls
-           preload="metadata"
-           className="h-full w-full rounded-xl"
-         />
-       </div>
-     );
-   }
-   return (
-     <div className="relative h-36 w-full overflow-hidden rounded-xl border border-ox-gray md:h-40 md:w-56">
-       <Image
-         src={issue.heroMediaUrl}
-         alt={issue.title}
-         fill
-         sizes="(min-width: 768px) 224px, 100vw"
-         className="object-cover"
-       />
-     </div>
-   );
- }

Option B — render next to meta (example):

-      <div className="flex items-center justify-between p-5">
+      <div className="flex items-center justify-between p-5 gap-4">
+        {/* Preview (optional) */}
+        {/* <MediaPreview issue={issue} /> */}

70-80: A11y + dedupe: add aria-label and consider reusing EngagementBar.

Improves accessibility and avoids duplicate share logic already implemented in EngagementBar.

-        <button
+        <button
           onClick={(e) => {
             e.preventDefault();
             handleShare(e);
           }}
           className="ml-4 flex items-center justify-center px-4 py-2 hover:bg-ox-black-2 rounded-lg transition"
+          aria-label={`Share ${issue.title}`}
         >

Also consider replacing this button with for consistency.

apps/web/src/data/newsletters.ts (4)

23-51: Populate tags in mock data (minimal).

Ensures filtering doesn’t crash. Adjust as needed.

     publishedAt: "2025-09-28",
     readTime: "6 min read",
     heroMediaUrl: "/images/Screenshot-2025-10-22.png",
     heroMediaType: "image",
+    tags: ["premium", "week-1", "automation"],

52-83: Populate tags for week 2.

     publishedAt: "2025-10-05",
     readTime: "7 min read",
     heroMediaUrl: "/images/Screenshot-2025-10-22.png",
     heroMediaType: "image",
+    tags: ["premium", "week-2", "community"],

84-114: Populate tags for week 3.

     publishedAt: "2025-10-12",
     readTime: "8 min read",
     heroMediaUrl: "https://assets.opensox.dev/newsletters/122-week3-hero.mp4",
     heroMediaType: "video",
+    tags: ["premium", "week-3", "beta"],

115-147: Populate tags for week 4.

     publishedAt: "2025-10-19",
     readTime: "5 min read",
     heroMediaUrl: "https://assets.opensox.dev/newsletters/122-week4-hero.jpg",
     heroMediaType: "image",
+    tags: ["premium", "week-4", "events"],
apps/api/src/services/newsletter.service.ts (2)

7-16: Normalize tag search tokens (and store tags lowercased).

hasSome is case-sensitive; normalize tokens to reduce misses. Ensure DB tags are stored lowercased.

-    const where: Prisma.NewsletterIssueWhereInput | undefined = search
+    const where: Prisma.NewsletterIssueWhereInput | undefined = search
       ? {
           OR: [
             { title: { contains: search, mode: "insensitive" } },
             { summary: { contains: search, mode: "insensitive" } },
-            { tags: { hasSome: search.split(" ").filter(Boolean) } },
+            { tags: { hasSome: search.split(" ").filter(Boolean).map(t => t.toLowerCase()) } },
           ],
         }
       : undefined;

18-33: Add basic pagination to prevent large scans.

Default to a reasonable limit; expose take/skip via router later.

     return prisma.newsletterIssue.findMany({
       ...(where && { where }),
       orderBy: { publishedAt: "desc" },
+      take: 50,
       select: {

Consider indexes: publishedAt (DESC), GIN on tags for array ops, trigram/GIN for title/summary if needed.

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

8-15: Remove unused import; keep only lazy import.

Avoid dead imports and bundle bloat.

-import { NewsletterSectionRenderer } from "@/components/newsletter/NewsletterSectionRenderer";
apps/web/src/app/(main)/dashboard/newsletters/page.tsx (2)

54-66: Always render the search input.

Improves discoverability and consistent layout.

-        {query && (
+        {(
           <div className="mt-4 flex w-full max-w-lg items-center rounded-full border border-ox-gray bg-ox-black-2 px-4 py-2">
             <input
               type="search"
               value={query}
               onChange={(e) => setQuery(e.target.value)}
               placeholder="Search newsletters"
               disabled={isLoading}
               className="w-full bg-transparent text-sm text-ox-white placeholder:text-ox-gray-light focus:outline-none"
             />
           </div>
-        )}
+        )}

52-67: Consider reusing NewsletterHeader for nav/search consistency.

Keeps UX consistent with detail pages and centralizes header logic.

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

7-14: Type ReactPlayer instead of casting to any.

Improves safety; optional.

-const ReactPlayer = dynamic(
-  () => import("react-player").then((mod) => mod.default),
-  {
-    ssr: false,
-    loading: () => null,
-  }
-) as any;
+const ReactPlayer = dynamic(() => import("react-player"), {
+  ssr: false,
+  loading: () => null,
+}) as unknown as typeof import("react-player")["default"];
apps/web/src/components/newsletter/NewsletterHeader.tsx (2)

52-61: Add aria-current for active nav item.

Improves screen reader feedback.

               <Link
                 key={link.href}
                 href={link.href}
                 target={link.external ? "_blank" : undefined}
                 rel={link.external ? "noopener noreferrer" : undefined}
                 className={cn(
                   "rounded-full border border-ox-gray px-4 py-2 text-xs font-semibold uppercase tracking-wide transition hover:border-ox-purple hover:text-ox-purple md:text-sm",
                   isActive ? "bg-ox-purple text-ox-white border-ox-purple" : "text-ox-gray-light"
                 )}
+                aria-current={isActive ? "page" : undefined}
               >

81-85: Make chips configurable via props.

Avoid hard-coded “Issue #122” and months; pass via props if needed.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a10539e and c34962c.

⛔ Files ignored due to path filters (2)
  • apps/web/public/images/Screenshot-2025-10-22.png is excluded by !**/*.png
  • apps/web/public/images/welcome.jpg is excluded by !**/*.jpg
📒 Files selected for processing (22)
  • apps/api/src/routers/_app.ts (2 hunks)
  • apps/api/src/routers/newsletter.ts (1 hunks)
  • apps/api/src/services/newsletter.service.ts (1 hunks)
  • apps/web/next.config.js (1 hunks)
  • apps/web/package.json (1 hunks)
  • apps/web/src/app/(main)/dashboard/layout.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/[slug]/NewsletterDetailClient.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/engagementBar.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/page.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/sidebar.tsx (1 hunks)
  • apps/web/src/app/SessionWrapper.tsx (1 hunks)
  • apps/web/src/app/api/newsletter.ts (1 hunks)
  • apps/web/src/app/globals.css (1 hunks)
  • apps/web/src/components/dashboard/Sidebar.tsx (1 hunks)
  • apps/web/src/components/newsletter/NewsletterCard.tsx (1 hunks)
  • apps/web/src/components/newsletter/NewsletterHeader.tsx (1 hunks)
  • apps/web/src/components/newsletter/NewsletterHero.tsx (1 hunks)
  • apps/web/src/components/newsletter/NewsletterSectionRenderer.tsx (1 hunks)
  • apps/web/src/data/newsletters.ts (1 hunks)
  • apps/web/src/types/next-auth.d.ts (1 hunks)
  • apps/web/tailwind.config.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (12)
apps/web/src/app/SessionWrapper.tsx (2)
apps/web/src/components/providers/SessionProvider.tsx (1)
  • SessionProvider (11-13)
apps/web/src/lib/auth/config.ts (1)
  • session (36-42)
apps/api/src/routers/newsletter.ts (2)
apps/api/src/trpc.ts (2)
  • router (35-35)
  • publicProcedure (36-36)
apps/api/src/services/newsletter.service.ts (1)
  • newsletterService (6-45)
apps/web/src/app/(main)/dashboard/newsletters/page.tsx (2)
apps/web/src/data/newsletters.ts (1)
  • NewsletterIssue (12-21)
apps/web/src/app/api/newsletter.ts (1)
  • useNewsletterList (8-23)
apps/api/src/routers/_app.ts (1)
apps/api/src/routers/newsletter.ts (1)
  • newsletterRouter (5-13)
apps/web/src/components/newsletter/NewsletterHero.tsx (1)
apps/web/src/data/newsletters.ts (1)
  • NewsletterIssue (12-21)
apps/web/src/app/(main)/dashboard/layout.tsx (2)
apps/web/src/components/ui/FiltersContainer.tsx (1)
  • FiltersContainer (21-144)
apps/web/src/components/dashboard/Sidebar.tsx (1)
  • Sidebar (31-117)
apps/web/src/components/newsletter/NewsletterHeader.tsx (1)
apps/web/src/lib/utils.ts (1)
  • cn (4-6)
apps/web/src/app/api/newsletter.ts (2)
apps/web/src/data/newsletters.ts (1)
  • newsletterIssues (23-147)
apps/web/src/lib/trpc.ts (1)
  • trpc (4-4)
apps/web/src/app/(main)/dashboard/newsletters/[slug]/NewsletterDetailClient.tsx (4)
apps/web/src/data/newsletters.ts (1)
  • NewsletterIssue (12-21)
apps/web/src/app/api/newsletter.ts (1)
  • useNewsletterDetail (25-27)
apps/web/src/components/newsletter/NewsletterHero.tsx (1)
  • NewsletterHero (12-39)
apps/web/src/app/(main)/dashboard/newsletters/engagementBar.tsx (1)
  • EngagementBar (11-49)
apps/web/src/components/newsletter/NewsletterCard.tsx (1)
apps/web/src/data/newsletters.ts (1)
  • NewsletterIssue (12-21)
apps/web/src/components/newsletter/NewsletterSectionRenderer.tsx (1)
apps/web/src/data/newsletters.ts (1)
  • NewsletterSection (1-10)
apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx (2)
apps/web/src/data/newsletters.ts (1)
  • newsletterIssues (23-147)
apps/web/src/app/(main)/dashboard/newsletters/[slug]/NewsletterDetailClient.tsx (1)
  • NewsletterDetailClient (65-82)
🔇 Additional comments (12)
apps/web/src/app/SessionWrapper.tsx (1)

14-16: LGTM!

The JSX formatting change improves readability while maintaining the component's functionality. The SessionProvider wrapper and session/children props handling remain unchanged.

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

74-77: Good practice for layout stability.

Adding scrollbar-gutter: stable prevents layout shift when scrollbar appears/disappears, which is beneficial for the dynamic content in the newsletter and dashboard sections.

Please verify that your project's target browsers support this property. scrollbar-gutter has solid support in modern browsers (Chrome 101+, Firefox, Edge) but may have limited support in older Safari versions. The change degrades gracefully in unsupported browsers, so if you're not targeting very old browsers, this is fine.

Would you like me to check the current browser support for scrollbar-gutter?

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

20-27: LGTM! Scroll handling implemented correctly.

The overflow and flex adjustments create a proper dashboard layout with independent scroll regions for the sidebar and main content while preventing the outer container from scrolling.

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

11-24: Note: Duplicate route definition.

This SIDEBAR_ROUTES constant duplicates the definition in apps/web/src/app/(main)/dashboard/sidebar.tsx (which is unused). See the comment on that file for recommendations to eliminate this duplication.


26-29: LGTM! Improved active route detection for nested paths.

Switching from strict equality to startsWith correctly handles nested routes. For example, when navigating to /dashboard/projects/123, the "Find Projects" link will remain active. This is the appropriate pattern for route matching in the dashboard navigation.

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

57-57: No issues found. The previous value was invalid Tailwind syntax.

The original review comment assumes changing ox-white from gray (text-slate-400) to white breaks existing components. However, "text-slate-400" is invalid Tailwind color syntax—it's a utility class name, not a valid color value. The change to "#ffffff" fixes this invalid configuration.

All 11 usages of ox-white throughout the codebase are part of the newsletter feature being added in this PR and are designed for white text on dark backgrounds (e.g., bg-ox-black-1, bg-ox-black-2). No legacy code depends on the previous broken value, so there is no breaking change.

Likely an incorrect or invalid review comment.

apps/web/next.config.js (1)

9-12: LGTM! Remote image pattern added correctly.

The new remote pattern for assets.opensox.dev is properly configured and aligns with Next.js 15 image optimization requirements for loading external media assets used in the newsletter feature.

apps/api/src/routers/_app.ts (2)

7-7: LGTM! Newsletter router properly imported.


23-23: LGTM! Newsletter router correctly registered.

The newsletter router is properly integrated into the app router, making the newsletter API endpoints available.

apps/web/package.json (1)

35-35: Version is current and secure—no action needed.

Verification confirms that react-player@3.3.3 is the latest available version on npm with no known security vulnerabilities in the GitHub advisory database.

apps/api/src/services/newsletter.service.ts (1)

35-44: Good: bySlug includes ordered sections.

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

41-47: No action required—assets.opensox.dev is already configured in the Next.js image remote patterns allowlist.

The verification confirms that assets.opensox.dev is explicitly listed in apps/web/next.config.js under remotePatterns with the https protocol, permitting the Image component to load remote images from this domain.

Copy link

@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

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

5-10: Fix PageProps: params should be a synchronous object, not a Promise.

Per the previous review, Next.js 15 page props receive params as a synchronous object, not a Promise. The async Request APIs (params(), searchParams() functions) are different from the props shape.

Apply this diff:

 interface PageProps {
-  params: Promise<{ slug: string }>;
+  params: { slug: string };
 }
 
 export default async function NewsletterDetail({ params }: PageProps) {
-  const { slug } = await params;
+  const { slug } = params;
   const fallback = newsletterIssues.find((issue) => issue.slug === slug);

Based on Next.js 15 docs.

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

9-12: Domain ownership verified through active usage; optional pathname restriction recommended.

The domain assets.opensox.dev is confirmed to be team-controlled, as evidenced by active references throughout apps/web/src/data/newsletters.ts for newsletter media assets. The configuration is syntactically correct and necessary for serving newsletter images and videos.

The current pattern is sufficiently broad for the newsletter feature. However, as an optional security best practice, consider restricting to the /newsletters/** pathname to limit exposure to only the intended asset directory:

 {
   protocol: "https",
   hostname: "assets.opensox.dev",
+  pathname: "/newsletters/**",
 },
apps/api/src/routers/newsletter.ts (1)

11-11: Consider adding slug format validation.

The current validation accepts any string as a slug, but slugs typically follow a specific format (e.g., lowercase alphanumeric with hyphens). Adding format validation would catch invalid inputs earlier and provide better error messages to clients.

Apply this diff to add slug format validation:

-    .input(z.object({ slug: z.string() }))
+    .input(z.object({ 
+      slug: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, {
+        error: () => "Slug must be lowercase alphanumeric with hyphens (kebab-case)"
+      })
+    }))

Note: Adjust the regex pattern if your slug format differs.

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

8-12: Label improved but could be more specific.

The previous "GitHub" label has been updated to "Community", which resolves the mismatch. However, "Discord" would be more specific and immediately clear to users where the link leads.

Apply this diff for maximum clarity:

-  { href: "https://discord.gg/nC9x4hef", label: "Community", external: true },
+  { href: "https://discord.gg/nC9x4hef", label: "Discord", external: true },
apps/web/src/data/newsletters.ts (1)

1-10: Consider discriminated union for NewsletterSection to enforce required fields per type.

The current design allows all media-related fields to be optional, but depending on type, certain fields should be required (e.g., "image" requires src and alt, "video" requires src, "media" requires both src and mediaSrc). This could permit invalid data at compile time.

Refactor to a discriminated union:

-export type NewsletterSection = {
-  type: "text" | "image" | "video" | "media";
-  heading: string;
-  body: string;
-  src?: string;
-  alt?: string;
-  mediaSrc?: string;
-  mediaType?: "image" | "video";
-  mediaAlt?: string;
-};
+export type NewsletterSection =
+  | {
+      type: "text";
+      heading: string;
+      body: string;
+    }
+  | {
+      type: "image";
+      heading: string;
+      body: string;
+      src: string;
+      alt: string;
+    }
+  | {
+      type: "video";
+      heading: string;
+      body: string;
+      src: string;
+    }
+  | {
+      type: "media";
+      heading: string;
+      body: string;
+      src: string;
+      alt: string;
+      mediaSrc: string;
+      mediaType: "image" | "video";
+      mediaAlt?: string;
+    };
apps/web/src/app/(main)/dashboard/newsletters/page.tsx (1)

33-33: Type assertion suggests missing type definitions.

The manual type assertion indicates useNewsletterList may lack proper return type definitions. Consider adding explicit return types to the hook.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c34962c and 9ab7036.

⛔ Files ignored due to path filters (2)
  • apps/web/public/images/Screenshot-2025-10-22.png is excluded by !**/*.png
  • apps/web/public/images/welcome.jpg is excluded by !**/*.jpg
📒 Files selected for processing (21)
  • apps/api/src/routers/_app.ts (2 hunks)
  • apps/api/src/routers/newsletter.ts (1 hunks)
  • apps/api/src/services/newsletter.service.ts (1 hunks)
  • apps/web/next.config.js (1 hunks)
  • apps/web/package.json (1 hunks)
  • apps/web/src/app/(main)/dashboard/layout.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/[slug]/NewsletterDetailClient.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/engagementBar.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/page.tsx (1 hunks)
  • apps/web/src/app/SessionWrapper.tsx (1 hunks)
  • apps/web/src/app/api/newsletter.ts (1 hunks)
  • apps/web/src/app/globals.css (1 hunks)
  • apps/web/src/components/dashboard/Sidebar.tsx (1 hunks)
  • apps/web/src/components/newsletter/NewsletterCard.tsx (1 hunks)
  • apps/web/src/components/newsletter/NewsletterHeader.tsx (1 hunks)
  • apps/web/src/components/newsletter/NewsletterHero.tsx (1 hunks)
  • apps/web/src/components/newsletter/NewsletterSectionRenderer.tsx (1 hunks)
  • apps/web/src/data/newsletters.ts (1 hunks)
  • apps/web/src/types/next-auth.d.ts (1 hunks)
  • apps/web/tailwind.config.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/web/src/app/SessionWrapper.tsx
🚧 Files skipped from review as they are similar to previous changes (13)
  • apps/web/src/app/globals.css
  • apps/web/src/app/(main)/dashboard/newsletters/engagementBar.tsx
  • apps/web/src/app/api/newsletter.ts
  • apps/web/src/components/newsletter/NewsletterCard.tsx
  • apps/web/src/app/(main)/dashboard/newsletters/[slug]/NewsletterDetailClient.tsx
  • apps/web/src/app/(main)/dashboard/layout.tsx
  • apps/api/src/routers/_app.ts
  • apps/web/tailwind.config.ts
  • apps/web/src/components/newsletter/NewsletterSectionRenderer.tsx
  • apps/api/src/services/newsletter.service.ts
  • apps/web/src/types/next-auth.d.ts
  • apps/web/src/components/newsletter/NewsletterHero.tsx
  • apps/web/src/components/dashboard/Sidebar.tsx
🧰 Additional context used
🧬 Code graph analysis (4)
apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx (2)
apps/web/src/data/newsletters.ts (1)
  • newsletterIssues (23-147)
apps/web/src/app/(main)/dashboard/newsletters/[slug]/NewsletterDetailClient.tsx (1)
  • NewsletterDetailClient (65-82)
apps/web/src/app/(main)/dashboard/newsletters/page.tsx (2)
apps/web/src/data/newsletters.ts (1)
  • NewsletterIssue (12-21)
apps/web/src/app/api/newsletter.ts (1)
  • useNewsletterList (6-21)
apps/web/src/components/newsletter/NewsletterHeader.tsx (1)
apps/web/src/lib/utils.ts (1)
  • cn (4-6)
apps/api/src/routers/newsletter.ts (2)
apps/api/src/trpc.ts (2)
  • router (35-35)
  • publicProcedure (36-36)
apps/api/src/services/newsletter.service.ts (1)
  • newsletterService (6-45)
🔇 Additional comments (10)
apps/web/package.json (1)

35-35: Version 3.3.3 is the latest stable release and no security vulnerabilities are reported for react-player v3.3.3. The dependency is correctly placed, properly formatted, and uses an appropriate version specifier.

apps/api/src/routers/newsletter.ts (1)

1-3: LGTM!

The imports are properly structured and follow ES module conventions.

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

11-17: LGTM!

The fallback lookup and notFound() flow is correct, and delegating to NewsletterDetailClient is appropriate.

apps/web/src/data/newsletters.ts (2)

12-21: LGTM!

The NewsletterIssue type is well-structured and covers all necessary fields.


23-147: LGTM!

The newsletter data is well-formed and provides good variety for testing the UI (text, image, video, and media sections).

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

7-11: LGTM!

Lazy loading NewsletterCard with code splitting is a good performance optimization.


13-29: LGTM!

CardSkeleton and NewsletterCardList components are well-structured with appropriate Suspense boundaries.


39-48: LGTM!

The filtering logic correctly searches by title and summary with proper memoization.


67-75: LGTM!

Conditional rendering for loading, results, and empty states is well-structured.


80-103: LGTM!

SkeletonList and EmptyState components provide good user feedback for loading and empty states.

Copy link

@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 (6)
apps/web/src/components/newsletter/NewsletterHeader.tsx (3)

14-21: Clarify disabled applies only when search is enabled.

Document or model the dependency so consumers aren’t confused.

+/**
+ * Header for newsletters.
+ * Note: `disabled` only applies when `onSearch` is provided.
+ */
 type Props = {

70-77: Add an accessible label to the search input.

Placeholders aren’t labels; add aria-label for screen readers.

             <input
               type="search"
               value={searchTerm}
               onChange={handleSearchChange}
               placeholder={searchPlaceholder}
               disabled={disabled}
+              aria-label={searchPlaceholder}
               className="w-full bg-transparent text-sm text-ox-white placeholder:text-ox-gray-light focus:outline-none"
             />

79-83: Remove placeholder chips or make them dynamic.

Hardcoded chips are misleading and non-interactive. Easiest fix: remove them for now.

-          <div className="flex flex-wrap gap-2 text-xs text-ox-gray-light">
-            <span className="rounded-full border border-ox-gray px-3 py-1">Premium</span>
-            <span className="rounded-full border border-ox-gray px-3 py-1">Issue #122</span>
-            <span className="rounded-full border border-ox-gray px-3 py-1">September · October</span>
-          </div>
apps/api/src/routers/newsletter.ts (2)

6-8: Remove double-optional input on list.

Require the object; keep search optional. Cleaner client API.

-  list: publicProcedure
-    .input(z.object({ search: z.string().optional() }).optional())
-    .query(async ({ input }) => newsletterService.list(input?.search)),
+  list: publicProcedure
+    .input(z.object({ search: z.string().optional() }))
+    .query(async ({ input }) => newsletterService.list(input.search)),

10-13: Return 404-style error when slug not found.

Throw TRPCError NOT_FOUND instead of returning null.

+import { TRPCError } from "@trpc/server";
 ...
   bySlug: publicProcedure
     .input(z.object({ slug: z.string() }))
-    .query(async ({ input }) => newsletterService.bySlug(input.slug)),
+    .query(async ({ input }) => {
+      const newsletter = await newsletterService.bySlug(input.slug);
+      if (!newsletter) {
+        throw new TRPCError({
+          code: "NOT_FOUND",
+          message: `Newsletter with slug "${input.slug}" not found`,
+        });
+      }
+      return newsletter;
+    }),
apps/web/src/app/(main)/dashboard/newsletters/page.tsx (1)

54-65: Search input never renders; show it unconditionally and add a label.

{query && (...)} hides the input when empty. Always render so users can type.

-        {query && (
-          <div className="mt-4 flex w-full max-w-lg items-center rounded-full border border-ox-gray bg-ox-black-2 px-4 py-2">
+        <div className="mt-4 flex w-full max-w-lg items-center rounded-full border border-ox-gray bg-ox-black-2 px-4 py-2" role="search">
             <input
               type="search"
               value={query}
               onChange={(e) => setQuery(e.target.value)}
               placeholder="Search newsletters"
               disabled={isLoading}
+              aria-label="Search newsletters"
               className="w-full bg-transparent text-sm text-ox-white placeholder:text-ox-gray-light focus:outline-none"
             />
           </div>
-        )}
🧹 Nitpick comments (5)
apps/web/src/components/newsletter/NewsletterHeader.tsx (1)

47-61: Expose active state to assistive tech.

Add aria-current="page" on the active internal link.

-              <Link
+              <Link
                 key={link.href}
                 href={link.href}
                 target={link.external ? "_blank" : undefined}
                 rel={link.external ? "noopener noreferrer" : undefined}
+                aria-current={isActive ? "page" : undefined}
                 className={cn(
apps/web/src/app/(main)/dashboard/layout.tsx (1)

20-28: Enable reliable vertical scrolling in flex layout.

With nested flex + overflow-y-auto, parents need min-h-0, and the row should have a fixed height to make child scrollbars work.

-      <div className="flex flex-row w-full overflow-hidden">
+      <div className="flex flex-row w-full overflow-hidden min-h-0 h-[calc(100vh-4rem)]">
         {showFilters && <FiltersContainer></FiltersContainer>}
         <aside
-          className={`flex-shrink-0 w-48 md:w-[40%] xl:w-[20%] ${showSidebar ? "block relative" : "hidden"} xl:block overflow-y-auto`}
+          className={`flex-shrink-0 w-48 md:w-[40%] xl:w-[20%] ${showSidebar ? "block relative" : "hidden"} xl:block overflow-y-auto min-h-0`}
         >
           <Sidebar></Sidebar>
         </aside>
-        <main className="flex-grow overflow-y-auto">{children}</main>
+        <main className="flex-grow overflow-y-auto min-h-0">{children}</main>
       </div>

Please verify on mobile: content should scroll, sidebar independently scrolls, no page lock.

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

76-86: Improve UX/accessibility on nav clicks.

  • Add aria-current for the active link.
  • Auto-close the sidebar after navigating on mobile.
-            <Link
+            <Link
               href={route.path}
-              className={getSidebarLinkClassName(pathname, route.path)}
+              className={getSidebarLinkClassName(pathname, route.path)}
+              aria-current={pathname.startsWith(route.path) ? "page" : undefined}
+              onClick={() => setShowSidebar(false)}
               key={route.path}
             >
apps/web/src/app/api/newsletter.ts (1)

6-21: Unify data source and expose real loading/error states.

Currently the list uses mock data with isLoading=false while detail uses TRPC. This is inconsistent and causes double filtering in the page.

Option A (recommended): switch list to TRPC and let the server filter.

-import { newsletterIssues } from "@/data/newsletters";
 import { trpc } from "@/lib/trpc";
 
 export const useNewsletterList = (search?: string) => {
-  // Use mock data immediately for instant load
-  const filteredData = search
-    ? newsletterIssues.filter((issue) =>
-        `${issue.title} ${issue.summary}`
-          .toLowerCase()
-          .includes(search.toLowerCase())
-      )
-    : newsletterIssues;
-
-  return {
-    data: filteredData,
-    isLoading: false,
-    error: null,
-  };
+  const query = trpc.newsletter.list.useQuery(
+    { search },
+    { keepPreviousData: true, staleTime: 30_000 }
+  );
+  return query;
 };

Option B: keep mock data, but rename to getNewsletterList and remove isLoading/error to avoid implying async behavior. Update callers accordingly.

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

33-37: Avoid double filtering and remove assertion cast.

Let this page handle filtering; fetch the list once without passing query, and don’t assert types.

-  const { data, isLoading } = useNewsletterList(query) as {
-    data: NewsletterIssue[] | undefined;
-    isLoading: boolean;
-  };
-  const issues = data?.length ? data : mockIssues;
+  const { data, isLoading } = useNewsletterList();
+  const issues = (data && data.length > 0 ? data : mockIssues) as NewsletterIssue[];
@@
-  const filteredIssues = useMemo<NewsletterIssue[]>(() => {
+  const filteredIssues = useMemo<NewsletterIssue[]>(() => {
     const normalized = query.trim().toLowerCase();
     if (!normalized) return issues as NewsletterIssue[];
 
     return (issues as NewsletterIssue[]).filter((issue) =>
       `${issue.title} ${issue.summary}`
         .toLowerCase()
         .includes(normalized)
     );
   }, [issues, query]);
@@
-        {isLoading ? (
+        {isLoading ? (
           <SkeletonList />
         ) : filteredIssues.length > 0 ? (
           <NewsletterCardList issues={filteredIssues} />
         ) : (
           <EmptyState query={query} />
         )}

Also applies to: 39-48, 67-75

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9ab7036 and b6e4500.

⛔ Files ignored due to path filters (2)
  • apps/web/public/images/Screenshot-2025-10-22.png is excluded by !**/*.png
  • apps/web/public/images/welcome.jpg is excluded by !**/*.jpg
📒 Files selected for processing (21)
  • apps/api/src/routers/_app.ts (2 hunks)
  • apps/api/src/routers/newsletter.ts (1 hunks)
  • apps/api/src/services/newsletter.service.ts (1 hunks)
  • apps/web/next.config.js (1 hunks)
  • apps/web/package.json (1 hunks)
  • apps/web/src/app/(main)/dashboard/layout.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/[slug]/NewsletterDetailClient.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/engagementBar.tsx (1 hunks)
  • apps/web/src/app/(main)/dashboard/newsletters/page.tsx (1 hunks)
  • apps/web/src/app/SessionWrapper.tsx (1 hunks)
  • apps/web/src/app/api/newsletter.ts (1 hunks)
  • apps/web/src/app/globals.css (1 hunks)
  • apps/web/src/components/dashboard/Sidebar.tsx (1 hunks)
  • apps/web/src/components/newsletter/NewsletterCard.tsx (1 hunks)
  • apps/web/src/components/newsletter/NewsletterHeader.tsx (1 hunks)
  • apps/web/src/components/newsletter/NewsletterHero.tsx (1 hunks)
  • apps/web/src/components/newsletter/NewsletterSectionRenderer.tsx (1 hunks)
  • apps/web/src/data/newsletters.ts (1 hunks)
  • apps/web/src/types/next-auth.d.ts (1 hunks)
  • apps/web/tailwind.config.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/web/src/app/SessionWrapper.tsx
🚧 Files skipped from review as they are similar to previous changes (12)
  • apps/web/src/components/newsletter/NewsletterHero.tsx
  • apps/web/src/app/(main)/dashboard/newsletters/[slug]/page.tsx
  • apps/web/package.json
  • apps/web/src/app/(main)/dashboard/newsletters/[slug]/NewsletterDetailClient.tsx
  • apps/api/src/services/newsletter.service.ts
  • apps/web/src/data/newsletters.ts
  • apps/web/src/components/newsletter/NewsletterCard.tsx
  • apps/web/src/components/newsletter/NewsletterSectionRenderer.tsx
  • apps/web/src/app/(main)/dashboard/newsletters/engagementBar.tsx
  • apps/web/next.config.js
  • apps/web/src/app/globals.css
  • apps/web/src/types/next-auth.d.ts
🧰 Additional context used
🧬 Code graph analysis (6)
apps/web/src/app/api/newsletter.ts (2)
apps/web/src/data/newsletters.ts (1)
  • newsletterIssues (23-147)
apps/web/src/lib/trpc.ts (1)
  • trpc (4-4)
apps/api/src/routers/newsletter.ts (2)
apps/api/src/trpc.ts (2)
  • router (35-35)
  • publicProcedure (36-36)
apps/api/src/services/newsletter.service.ts (1)
  • newsletterService (6-45)
apps/web/src/app/(main)/dashboard/newsletters/page.tsx (2)
apps/web/src/data/newsletters.ts (1)
  • NewsletterIssue (12-21)
apps/web/src/app/api/newsletter.ts (1)
  • useNewsletterList (6-21)
apps/web/src/components/newsletter/NewsletterHeader.tsx (1)
apps/web/src/lib/utils.ts (1)
  • cn (4-6)
apps/api/src/routers/_app.ts (1)
apps/api/src/routers/newsletter.ts (1)
  • newsletterRouter (5-13)
apps/web/src/app/(main)/dashboard/layout.tsx (2)
apps/web/src/components/ui/FiltersContainer.tsx (1)
  • FiltersContainer (21-144)
apps/web/src/components/dashboard/Sidebar.tsx (1)
  • Sidebar (31-117)
🔇 Additional comments (3)
apps/web/tailwind.config.ts (1)

57-57: No action required—the color change is correct and appropriate.

The change from "text-slate-400" to "#ffffff" is not a breaking change. The previous value was invalid (it's a Tailwind utility class name, not a valid color value), and the new value correctly implements the semantic meaning of the token name "ox-white."

All 24+ usages across the codebase apply the token as text color (text-ox-white), primarily in new newsletter components and existing dashboard components. The white color on dark backgrounds (bg-ox-black-1, bg-ox-black-2) provides appropriate contrast and readability. No components are negatively impacted by this change.

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

20-23: Newsletter route: LGTM.

Good addition; prefix matching will cover nested routes like /dashboard/newsletters/[slug].

apps/api/src/routers/_app.ts (1)

7-7: Wire-up of newsletter router looks correct.

Import and registration into appRouter are consistent with existing pattern.

Also applies to: 23-24

@apsinghdev
Copy link
Owner

@Shivam107 hi shivam, aman is currently working on the overall dashboard. so once, it's done. i'll dive deep into it and will leave reviews because we'll have to make some changes after the dashboard revamp. nice ui btw ❤️

@Shivam107
Copy link
Author

@Shivam107 hi shivam, aman is currently working on the overall dashboard. so once, it's done. i'll dive deep into it and will leave reviews because we'll have to make some changes after the dashboard revamp. nice ui btw ❤️

yes , that's what i was thinking

Copy link

@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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b6e4500 and c9b7410.

📒 Files selected for processing (3)
  • apps/api/src/routers/_app.ts (2 hunks)
  • apps/web/package.json (1 hunks)
  • apps/web/src/components/dashboard/Sidebar.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/src/components/dashboard/Sidebar.tsx
  • apps/web/package.json
🧰 Additional context used
🧬 Code graph analysis (1)
apps/api/src/routers/_app.ts (1)
apps/api/src/routers/newsletter.ts (1)
  • newsletterRouter (5-13)
🔇 Additional comments (1)
apps/api/src/routers/_app.ts (1)

8-8: LGTM!

The newsletter router integration follows the established pattern and is correctly wired into the app router. The implementation is clean and consistent with existing router registrations.

Also applies to: 24-24

import { userRouter } from "./user.js";
import { projectRouter } from "./projects.js";
import { authRouter } from "./auth.js";
import { paymentRouter } from "./payment.js";
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove unused import.

The paymentRouter import is no longer used in the appRouter after being replaced with newsletterRouter. This is dead code that should be removed.

Apply this diff to remove the unused import:

-import { paymentRouter } from "./payment.js";
📝 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
import { paymentRouter } from "./payment.js";
🤖 Prompt for AI Agents
In apps/api/src/routers/_app.ts around line 6, the import "paymentRouter" is
unused after replacing it with "newsletterRouter"; remove the unused import
statement from the file (delete the import line for paymentRouter) and verify
there are no remaining references to paymentRouter in the file or exports so the
file compiles and lints cleanly.

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.

v2-dashboard: create a page for newsletter

2 participants