Skip to content

add rebrand landing page#211

Merged
KMKoushik merged 6 commits intomainfrom
km/2025-09-04-rebrand-landing
Sep 5, 2025
Merged

add rebrand landing page#211
KMKoushik merged 6 commits intomainfrom
km/2025-09-04-rebrand-landing

Conversation

@KMKoushik
Copy link
Copy Markdown
Member

@KMKoushik KMKoushik commented Sep 4, 2025

Summary by CodeRabbit

  • New Features

    • Redesigned marketing homepage with modular sections, CTAs, pricing, code example, and footer.
    • New code highlighting component and reusable feature cards; marketing TopNav, Privacy, and Terms pages added.
    • SQL seed script to populate dashboard demo data.
  • Improvements

    • Server-rendered GitHub button with periodic revalidation and compact star count.
    • Marketing metadata and isolated theme storage; email chart gains rounded-top bar visuals; primary-light color token added.
  • Style

    • Updated brand color tokens; monospace applied to key metrics; H1 uses primary color.
  • Chores

    • Removed legacy code-theme and internal contributor guidelines; updated dependencies.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Sep 4, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Deletes ./.windsurfrules and applies widespread UI, theming, and tooling changes: reworks the marketing site (layout, page, new components), converts GitHubStarsButton to a server component with 7‑day revalidation, replaces legacy code-highlighting with a Shiki-based CodeBlock, adds a dashboard seed and chart tweaks, and updates tokens/styles.

Changes

Cohort / File(s) Summary
Repo rules cleanup
./.windsurfrules
Deleted all content; file becomes empty.
Marketing site — layout & page
apps/marketing/src/app/layout.tsx, apps/marketing/src/app/page.tsx
layout: metadataBase, enhanced Open Graph/Twitter metadata, html/body class tweaks, ThemeProvider set to storageKey="marketing-theme". page: replaced single layout with modular sections (TopNav, Hero, TrustedBy, Features, CodeExample, Pricing, About, Footer), added repo/app constants, removed FAQ.
Marketing site — components
apps/marketing/src/components/GitHubStarsButton.tsx, apps/marketing/src/components/FeatureCard.tsx, apps/marketing/src/components/FeatureCardPlain.tsx, apps/marketing/src/components/TopNav.tsx
GitHubStarsButton: converted from client to server (export async function), server-side fetch with optional GITHUB_TOKEN, headers, and revalidation (7 days); formats compact counts. Added client components FeatureCard, FeatureCardPlain, and TopNav.
Code highlighting — remove old, add Shiki-based
packages/ui/code-theme.ts (deleted), packages/ui/src/code.tsx (deleted), packages/ui/src/code-block.tsx (added), packages/ui/package.json
Removed legacy code-theme and tabbed Code component. Added CodeBlock using Shiki + hast-util-to-jsx-runtime. package.json: removed react-syntax-highlighter and types; added shiki and hast-util-to-jsx-runtime.
Dashboard — seed & chart UI
apps/web/prisma/seed_dashboard.sql, apps/web/src/app/(dashboard)/dashboard/email-chart.tsx, apps/web/src/app/(dashboard)/dashboard/reputation-metrics.tsx
Added PostgreSQL seed script for team/domain/metrics/daily usage/campaign/emails. EmailChart: introduced STACK_ORDER and createRoundedTopShape to render rounded-top stacked bars via custom Rectangle shape. ReputationMetrics: typography tweak applying font-mono to bounce/complaint displays.
UI libs — theming & typography
packages/tailwind-config/tailwind.config.ts, packages/ui/src/typography.tsx, packages/ui/styles/globals.css
Tailwind: added colors.primary-light. Typography: H1 now includes text-primary. globals.css: adjusted --primary HSL values for light/dark, added --primary-light tokens, and introduced dark-mode Shiki-related CSS vars.
Legal / marketing pages
apps/marketing/src/app/privacy/page.tsx, apps/marketing/src/app/terms/page.tsx
Added Privacy Policy and Terms of Service pages with metadata and styled static content; both export page components and metadata.
Misc small changes
apps/web/src/components/AppSideBar.tsx, apps/web/src/server/auth.ts
AppSideBar: added next/image import (unused). auth.ts: formatting-only change to GitHub provider scope string (no behavioral change).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant MP as Marketing Page (Next.js SSR)
  participant GSB as GitHubStarsButton (Server)
  participant GH as GitHub API

  U->>MP: GET /
  MP->>GSB: call GitHubStarsButton() (server render)
  note right of GSB #DDEFEF: build headers (Accept, API‑Version, UA,\noptional Authorization) and fetch with revalidate=604800
  GSB->>GH: GET /repos/:owner/:repo
  alt 200 OK with stargazers_count
    GH-->>GSB: { stargazers_count }
    GSB-->>MP: render button + compact count
  else Error/No data
    GH-->>GSB: Error/empty
    GSB-->>MP: render button with placeholder
  end
  MP-->>U: server-rendered HTML (cached per revalidation)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • add rebrand landing page #211: Large marketing/UI and code-highlighting updates — likely touches the same marketing components and Shiki/code-block migration.
  • rebrand to useSend #210: Repository rebrand and marketing refactor — likely intersects layout, metadata, and naming changes.
  • fix colors in dashboard #206: Dashboard UI and chart/theming changes — likely related to email-chart and style/token adjustments.

Poem

I hop through commits with nimble feet,
Tokens bright and charts made neat.
Stars are fetched and cached to stay,
New blocks of code that love to play.
A tiny rabbit cheers the day! 🐇✨


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4a94c47 and e985e3e.

📒 Files selected for processing (5)
  • apps/marketing/src/app/layout.tsx (2 hunks)
  • apps/marketing/src/app/page.tsx (1 hunks)
  • apps/marketing/src/app/privacy/page.tsx (1 hunks)
  • apps/marketing/src/app/terms/page.tsx (1 hunks)
  • apps/marketing/src/components/TopNav.tsx (1 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch km/2025-09-04-rebrand-landing

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Sep 4, 2025

Deploying unsend with  Cloudflare Pages  Cloudflare Pages

Latest commit: e985e3e
Status:🚫  Build failed.

View logs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
apps/web/src/app/(dashboard)/dashboard/reputation-metrics.tsx (2)

96-114: Fix possible runtime crash when metrics is undefined.

metrics?.bounceRate.toFixed(2) will call toFixed on undefined. Guard it.

-            <div className="text-2xl mt-2 font-mono">
-              {metrics?.bounceRate.toFixed(2)}%
-            </div>
+            <div className="text-2xl mt-2 font-mono">
+              {(metrics?.bounceRate != null ? metrics.bounceRate.toFixed(2) : "—")}%
+            </div>

Optional: show a skeleton/placeholder while isLoading is true.


245-261: Apply the same guard for complaint rate.

Avoid calling toFixed on undefined.

-            <div className="text-2xl mt-2 font-mono">
-              {metrics?.complaintRate.toFixed(2)}%
-            </div>
+            <div className="text-2xl mt-2 font-mono">
+              {(metrics?.complaintRate != null ? metrics.complaintRate.toFixed(2) : "—")}%
+            </div>
apps/marketing/src/components/GitHubStarsButton.tsx (1)

53-68: Avoid nested interactive elements (<a> inside <Button>).

Render the anchor as the button via asChild to fix a11y/HTML validity.

-  return (
-    <Button variant="outline" size="lg" className="px-4 gap-2">
-      <a
-        href={REPO_URL}
-        target="_blank"
-        rel="noopener noreferrer"
-        aria-label="Star this repo on GitHub"
-        className="flex items-center gap-2"
-      >
+  return (
+    <Button variant="outline" size="lg" className="px-4 gap-2" asChild>
+      <a
+        href={REPO_URL}
+        target="_blank"
+        rel="noopener noreferrer"
+        aria-label="Visit the GitHub repository"
+        className="inline-flex items-center gap-2"
+      >
         <GitHubIcon className="h-4 w-4" />
         <span>GitHub</span>
         <span className="rounded-md bg-muted px-1.5 py-0.5 text-xs tabular-nums text-muted-foreground">
           {formatted}
         </span>
-      </a>
-    </Button>
+      </a>
+    </Button>
apps/web/src/app/(dashboard)/dashboard/email-chart.tsx (1)

82-86: Guard against divide‑by‑zero to avoid Infinity.toFixed() runtime errors.

If sent is 0, percentage becomes Infinity and .toFixed() will throw. Use a safe ratio.

+// helper near the top (e.g., after hooks)
+const safeRatio = (num: number, den: number) => (den > 0 ? num / den : 0);

   <EmailChartItem
     status={EmailStatus.DELIVERED}
     count={statusQuery.data.totalCounts.delivered}
-    percentage={
-      statusQuery.data.totalCounts.delivered /
-      statusQuery.data.totalCounts.sent
-    }
+    percentage={safeRatio(
+      statusQuery.data.totalCounts.delivered,
+      statusQuery.data.totalCounts.sent
+    )}
   />
   <EmailChartItem
     status={EmailStatus.BOUNCED}
     count={statusQuery.data.totalCounts.bounced}
-    percentage={
-      statusQuery.data.totalCounts.bounced /
-      statusQuery.data.totalCounts.sent
-    }
+    percentage={safeRatio(
+      statusQuery.data.totalCounts.bounced,
+      statusQuery.data.totalCounts.sent
+    )}
   />
   <EmailChartItem
     status={EmailStatus.COMPLAINED}
     count={statusQuery.data.totalCounts.complained}
-    percentage={
-      statusQuery.data.totalCounts.complained /
-      statusQuery.data.totalCounts.sent
-    }
+    percentage={safeRatio(
+      statusQuery.data.totalCounts.complained,
+      statusQuery.data.totalCounts.sent
+    )}
   />
   <EmailChartItem
     status={EmailStatus.CLICKED}
     count={statusQuery.data.totalCounts.clicked}
-    percentage={
-      statusQuery.data.totalCounts.clicked /
-      statusQuery.data.totalCounts.sent
-    }
+    percentage={safeRatio(
+      statusQuery.data.totalCounts.clicked,
+      statusQuery.data.totalCounts.sent
+    )}
   />
   <EmailChartItem
     status={EmailStatus.OPENED}
     count={statusQuery.data.totalCounts.opened}
-    percentage={
-      statusQuery.data.totalCounts.opened /
-      statusQuery.data.totalCounts.sent
-    }
+    percentage={safeRatio(
+      statusQuery.data.totalCounts.opened,
+      statusQuery.data.totalCounts.sent
+    )}
   />

Also applies to: 91-94, 99-102, 107-110, 115-118

🧹 Nitpick comments (10)
apps/marketing/src/components/GitHubStarsButton.tsx (3)

8-24: Tighten compact formatting (cosmetic).

Drop the space before the unit for a more common “1.2K/1.2M” style.

-    { v: 1_000_000_000, s: " B" },
-    { v: 1_000_000, s: " M" },
-    { v: 1_000, s: " K" },
+    { v: 1_000_000_000, s: "B" },
+    { v: 1_000_000, s: "M" },
+    { v: 1_000, s: "K" },

6-7: Consider fresher revalidation.

If you want the star count to feel current, revalidate daily.

-const REVALIDATE_SECONDS = 60 * 60 * 24 * 7; // 7 days
+const REVALIDATE_SECONDS = 60 * 60 * 24; // 1 day

58-58: Clarify aria-label.

Action is “visit repo,” not “star.” Minor a11y wording tweak.

-        aria-label="Star this repo on GitHub"
+        aria-label="Visit the GitHub repository"
packages/ui/src/typography.tsx (1)

17-17: Remove stray leading space and confirm token availability.

Minor: the class string starts with a space. Also confirm text-primary is defined in Tailwind tokens for all themes.

-        " font-mono text-xl font-medium text-primary",
+        "font-mono text-xl font-medium text-primary",
apps/web/src/app/(dashboard)/dashboard/email-chart.tsx (2)

36-55: Tighten types for the shape factory; drop any and avoid as any.

Use props inferred from Rectangle and a concrete radius tuple.

-function createRoundedTopShape(currentKey: StackKey) {
+type ShapeProps = React.ComponentProps<typeof Rectangle> & {
+  payload?: Partial<Record<StackKey, number>>;
+};
+function createRoundedTopShape(currentKey: StackKey) {
   const currentIndex = STACK_ORDER.indexOf(currentKey);
-  return (props: any) => {
-    const payload = props.payload as
-      | Partial<Record<StackKey, number>>
-      | undefined;
+  return (props: ShapeProps) => {
+    const payload = props.payload;
     let hasAbove = false;
     for (let i = currentIndex + 1; i < STACK_ORDER.length; i++) {
       const key = STACK_ORDER[i];
       const val = key ? (payload?.[key] ?? 0) : 0;
       if (val > 0) {
         hasAbove = true;
         break;
       }
     }
 
-    const radius = hasAbove ? [0, 0, 0, 0] : [2.5, 2.5, 0, 0];
-    return <Rectangle {...props} radius={radius as any} />;
+    const radius: [number, number, number, number] = hasAbove
+      ? [0, 0, 0, 0]
+      : [2.5, 2.5, 0, 0];
+    return <Rectangle {...props} radius={radius} />;
   };
 }

233-257: Optional: avoid recreating shape functions on each render.

Precompute shapes once at module scope to reduce re-renders.

+// top-level (after createRoundedTopShape)
+const SHAPES = {
+  delivered: createRoundedTopShape("delivered"),
+  bounced: createRoundedTopShape("bounced"),
+  complained: createRoundedTopShape("complained"),
+  opened: createRoundedTopShape("opened"),
+  clicked: createRoundedTopShape("clicked"),
+} as const;
...
-                shape={createRoundedTopShape("delivered")}
+                shape={SHAPES.delivered}
...
-                shape={createRoundedTopShape("bounced")}
+                shape={SHAPES.bounced}
...
-                shape={createRoundedTopShape("complained")}
+                shape={SHAPES.complained}
...
-                shape={createRoundedTopShape("opened")}
+                shape={SHAPES.opened}
...
-                shape={createRoundedTopShape("clicked")}
+                shape={SHAPES.clicked}
apps/web/prisma/seed_dashboard.sql (1)

8-11: Consider making team insert idempotent to prevent unbounded duplicates.

If this runs multiple times, Team rows will balloon. If that's not desired beyond demos, add a conflict clause.

-INSERT INTO "Team" ("name", "plan", "isActive", "apiRateLimit", "createdAt", "updatedAt")
-VALUES ('Acme Inc', 'BASIC'::"Plan", TRUE, 10, NOW(), NOW());
+INSERT INTO "Team" ("name", "plan", "isActive", "apiRateLimit", "createdAt", "updatedAt")
+VALUES ('Acme Inc', 'BASIC'::"Plan", TRUE, 10, NOW(), NOW())
+ON CONFLICT ("name") DO UPDATE SET "updatedAt" = EXCLUDED."updatedAt";
apps/marketing/src/app/page.tsx (3)

321-323: Tailwind class text-blue likely doesn’t exist. Use a tokened color.

Switch to text-primary (or text-primary-light) to align with the new palette.

-              <div className="mb-3 text-blue">
+              <div className="mb-3 text-primary">
                 <f.icon className="h-6 w-6" />
               </div>

81-101: Hero image loading: prefer priority for LCP and drop loading="eager".

For above‑the‑fold hero, set priority and omit loading to avoid mixed hints.

-              loading="eager"
-              priority={false}
+              priority

Apply the same change to the dark variant below.

Also applies to: 92-100


57-63: Align with shared typography primitives (optional).

Consider <H1> from UI to keep heading styles consistent with the rebrand tokens.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1c8bb55 and 534caa6.

⛔ Files ignored due to path filters (2)
  • apps/marketing/public/hero-dark.png is excluded by !**/*.png
  • apps/marketing/public/hero-light.png is excluded by !**/*.png
📒 Files selected for processing (11)
  • .windsurfrules (0 hunks)
  • apps/marketing/src/app/layout.tsx (2 hunks)
  • apps/marketing/src/app/page.tsx (1 hunks)
  • apps/marketing/src/components/GitHubStarsButton.tsx (2 hunks)
  • apps/web/prisma/seed_dashboard.sql (1 hunks)
  • apps/web/src/app/(dashboard)/dashboard/email-chart.tsx (3 hunks)
  • apps/web/src/app/(dashboard)/dashboard/reputation-metrics.tsx (4 hunks)
  • apps/web/src/components/AppSideBar.tsx (1 hunks)
  • packages/tailwind-config/tailwind.config.ts (1 hunks)
  • packages/ui/src/typography.tsx (1 hunks)
  • packages/ui/styles/globals.css (4 hunks)
💤 Files with no reviewable changes (1)
  • .windsurfrules
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports, and ensure proper naming of key components.

Files:

  • apps/web/src/app/(dashboard)/dashboard/reputation-metrics.tsx
  • apps/web/src/components/AppSideBar.tsx
  • packages/ui/src/typography.tsx
  • packages/tailwind-config/tailwind.config.ts
  • apps/marketing/src/components/GitHubStarsButton.tsx
  • apps/marketing/src/app/layout.tsx
  • apps/marketing/src/app/page.tsx
  • apps/web/src/app/(dashboard)/dashboard/email-chart.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Prefer TypeScript for implementation files (TypeScript-first)
ESLint must pass with zero warnings using @usesend/eslint-config before PRs

Files:

  • apps/web/src/app/(dashboard)/dashboard/reputation-metrics.tsx
  • apps/web/src/components/AppSideBar.tsx
  • packages/ui/src/typography.tsx
  • packages/tailwind-config/tailwind.config.ts
  • apps/marketing/src/components/GitHubStarsButton.tsx
  • apps/marketing/src/app/layout.tsx
  • apps/marketing/src/app/page.tsx
  • apps/web/src/app/(dashboard)/dashboard/email-chart.tsx
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,md}: Use 2-space indentation and keep semicolons as enforced by Prettier 3
Run Prettier 3 formatting (pnpm format) over TypeScript and Markdown files

Files:

  • apps/web/src/app/(dashboard)/dashboard/reputation-metrics.tsx
  • apps/web/src/components/AppSideBar.tsx
  • packages/ui/src/typography.tsx
  • packages/tailwind-config/tailwind.config.ts
  • apps/marketing/src/components/GitHubStarsButton.tsx
  • apps/marketing/src/app/layout.tsx
  • apps/marketing/src/app/page.tsx
  • apps/web/src/app/(dashboard)/dashboard/email-chart.tsx
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Name React component files in PascalCase (e.g., AppSideBar.tsx)

Files:

  • apps/web/src/app/(dashboard)/dashboard/reputation-metrics.tsx
  • apps/web/src/components/AppSideBar.tsx
  • packages/ui/src/typography.tsx
  • apps/marketing/src/components/GitHubStarsButton.tsx
  • apps/marketing/src/app/layout.tsx
  • apps/marketing/src/app/page.tsx
  • apps/web/src/app/(dashboard)/dashboard/email-chart.tsx
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

In apps/web, use the import alias "/" for src imports (e.g., import { x } from "/utils/x")

Files:

  • apps/web/src/app/(dashboard)/dashboard/reputation-metrics.tsx
  • apps/web/src/components/AppSideBar.tsx
  • apps/web/src/app/(dashboard)/dashboard/email-chart.tsx
🧬 Code graph analysis (2)
apps/marketing/src/app/layout.tsx (1)
packages/ui/index.ts (1)
  • ThemeProvider (5-5)
apps/marketing/src/app/page.tsx (1)
apps/marketing/src/components/GitHubStarsButton.tsx (1)
  • GitHubStarsButton (26-69)
🔇 Additional comments (7)
packages/tailwind-config/tailwind.config.ts (1)

110-112: LGTM: token wiring looks correct.

"primary-light" is quoted (hyphenated key) and maps to the CSS var as expected.

packages/ui/styles/globals.css (1)

17-17: LGTM: color token updates align with Tailwind config.

Values present for both light and dark themes; "primary-light" defined.

Also applies to: 66-66, 79-79, 126-126

apps/marketing/src/app/layout.tsx (2)

18-48: Metadata updates look solid.

metadataBase, OG/Twitter blocks, and icon config are consistent with Next Metadata. No action needed.


58-66: ThemeProvider isolation LGTM.

Separate storageKey="marketing-theme" avoids cross‑app bleed. Good move.

apps/web/prisma/seed_dashboard.sql (1)

37-96: Nice: consistent, bounded 14‑day seeds with ON CONFLICT safety.

The seasonal/noise model and conflict handling look good.

Also applies to: 97-156

apps/marketing/src/app/page.tsx (2)

66-70: Use asChild on Button to render links
Button already supports the asChild prop—avoid nesting an <a> inside a <button> by wrapping your link like this:

<Button asChild size="lg" className="px-6">
  <a href={GET_STARTED_URL} target="_blank" rel="noopener noreferrer">
    Get started
  </a>
</Button>

Apply this change to all other similar instances.


210-216: No domain whitelist needed for marketing app
The next.config.js in apps/marketing already sets images.unoptimized: true for static export, which bypasses the image optimization pipeline (and its domain-validation step) (vercel.com). External images from Twitter or doras.to will render without build failures.

Likely an incorrect or invalid review comment.

Comment on lines +26 to +35
const STACK_ORDER: string[] = [
"delivered",
"bounced",
"complained",
"opened",
"clicked",
] as const;

type StackKey = (typeof STACK_ORDER)[number];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix incorrect const typing; current annotation conflicts with as const.

string[] + as const is incompatible and breaks StackKey. Let TS infer a readonly tuple.

-const STACK_ORDER: string[] = [
-  "delivered",
-  "bounced",
-  "complained",
-  "opened",
-  "clicked",
-] as const;
+const STACK_ORDER = [
+  "delivered",
+  "bounced",
+  "complained",
+  "opened",
+  "clicked",
+] as const;
 
-type StackKey = (typeof STACK_ORDER)[number];
+type StackKey = (typeof STACK_ORDER)[number];
📝 Committable suggestion

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

Suggested change
const STACK_ORDER: string[] = [
"delivered",
"bounced",
"complained",
"opened",
"clicked",
] as const;
type StackKey = (typeof STACK_ORDER)[number];
const STACK_ORDER = [
"delivered",
"bounced",
"complained",
"opened",
"clicked",
] as const;
type StackKey = (typeof STACK_ORDER)[number];
🤖 Prompt for AI Agents
In apps/web/src/app/(dashboard)/dashboard/email-chart.tsx around lines 26 to 35,
the explicit ": string[]" annotation conflicts with the "as const" assertion and
prevents STACK_ORDER from being inferred as a readonly tuple; remove the ":
string[]" type annotation so TS can infer STACK_ORDER as a readonly tuple via
"as const", leaving type StackKey = (typeof STACK_ORDER)[number] to work
correctly.

import { usePathname } from "next/navigation";
import { Badge } from "@usesend/ui/src/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@usesend/ui/src/avatar";
import Image from "next/image";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove unused import to satisfy ESLint zero-warnings policy.

Image isn’t used; this will trip the linter.

-import Image from "next/image";
📝 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 Image from "next/image";
🤖 Prompt for AI Agents
In apps/web/src/components/AppSideBar.tsx around line 42, the import "Image"
from "next/image" is unused and violates the ESLint zero-warnings policy; remove
that unused import line (or replace it with a used import) and re-run the linter
to ensure no remaining import warnings.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (7)
apps/marketing/src/components/FeatureCardPlain.tsx (2)

1-1: Consider making this a Server Component.

No client-only APIs are used; dropping the directive trims client JS. Keep it if this must live under a client boundary.

-"use client";
+/* Server Component by default */

11-11: Remove redundant class.

sm:p-1 duplicates p-1 with no change.

-    <div className="h-full rounded-2xl bg-primary/30 p-1 sm:p-1 border-2 border-primary/30 shadow-sm">
+    <div className="h-full rounded-2xl bg-primary/30 p-1 border-2 border-primary/30 shadow-sm">
apps/marketing/src/components/FeatureCard.tsx (5)

17-17: Clip rounded corners for filled image.

Add overflow-hidden on the aspect box so the image respects the rounded top.

-        <div className="relative w-full aspect-[16/9] rounded-t-xl">
+        <div className="relative w-full aspect-[16/9] rounded-t-xl overflow-hidden">

19-41: Add responsive sizes and improve alt semantics.

  • With fill, set sizes to prevent overserving.
  • Derive alt from title, else use empty alt for decorative defaults.
-            <Image
-              src={imageSrc}
-              alt={title || "Feature image"}
-              fill
-              className="object-cover rounded-t-xl"
-              priority={false}
-            />
+            <Image
+              src={imageSrc}
+              alt={title ? `${title} illustration` : ""}
+              fill
+              sizes="(min-width:1024px) 33vw, (min-width:640px) 50vw, 100vw"
+              className="object-cover rounded-t-xl"
+            />
@@
-              <Image
-                src="/hero-light.png"
-                alt="Feature image"
-                fill
-                className="object-cover dark:hidden rounded-t-xl"
-                priority={false}
-              />
+              <Image
+                src="/hero-light.png"
+                alt=""
+                fill
+                sizes="(min-width:1024px) 33vw, (min-width:640px) 50vw, 100vw"
+                className="object-cover dark:hidden rounded-t-xl"
+              />
@@
-              <Image
-                src="/hero-dark.png"
-                alt="Feature image"
-                fill
-                className="object-cover hidden dark:block rounded-t-xl"
-                priority={false}
-              />
+              <Image
+                src="/hero-dark.png"
+                alt=""
+                fill
+                sizes="(min-width:1024px) 33vw, (min-width:640px) 50vw, 100vw"
+                className="object-cover hidden dark:block rounded-t-xl"
+              />

5-13: Expose priority to callers (optional).

Allow above-the-fold usage to opt-in to eager loading without hardcoding.

-export function FeatureCard({
-  title,
-  content,
-  imageSrc,
-}: {
-  title?: string;
-  content?: string;
-  imageSrc?: string;
-}) {
+export function FeatureCard({
+  title,
+  content,
+  imageSrc,
+  priority,
+}: {
+  title?: string;
+  content?: string;
+  imageSrc?: string;
+  priority?: boolean;
+}) {

And pass it to images:

-            <Image
+            <Image
               src={imageSrc}
@@
-              className="object-cover rounded-t-xl"
+              className="object-cover rounded-t-xl"
+              priority={!!priority}
             />
@@
-              <Image
+              <Image
                 src="/hero-light.png"
@@
-                className="object-cover dark:hidden rounded-t-xl"
+                className="object-cover dark:hidden rounded-t-xl"
+                priority={!!priority}
               />
@@
-              <Image
+              <Image
                 src="/hero-dark.png"
@@
-                className="object-cover hidden dark:block rounded-t-xl"
+                className="object-cover hidden dark:block rounded-t-xl"
+                priority={!!priority}
               />

15-15: Remove redundant class.

sm:p-1 duplicates p-1.

-    <div className="h-full rounded-2xl bg-primary/30 p-1 sm:p-1 border-2 border-primary/30 shadow-sm">
+    <div className="h-full rounded-2xl bg-primary/30 p-1 border-2 border-primary/30 shadow-sm">

1-1: Consider making this a Server Component.

No client-only logic here; removing the directive reduces hydration. Keep if needed under a client boundary.

-"use client";
+/* Server Component by default */
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 534caa6 and cdb7b50.

📒 Files selected for processing (5)
  • apps/marketing/src/app/layout.tsx (2 hunks)
  • apps/marketing/src/app/page.tsx (1 hunks)
  • apps/marketing/src/components/FeatureCard.tsx (1 hunks)
  • apps/marketing/src/components/FeatureCardPlain.tsx (1 hunks)
  • apps/web/src/server/auth.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/web/src/server/auth.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/marketing/src/app/layout.tsx
  • apps/marketing/src/app/page.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports, and ensure proper naming of key components.

Files:

  • apps/marketing/src/components/FeatureCardPlain.tsx
  • apps/marketing/src/components/FeatureCard.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Prefer TypeScript for implementation files (TypeScript-first)
ESLint must pass with zero warnings using @usesend/eslint-config before PRs

Files:

  • apps/marketing/src/components/FeatureCardPlain.tsx
  • apps/marketing/src/components/FeatureCard.tsx
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,md}: Use 2-space indentation and keep semicolons as enforced by Prettier 3
Run Prettier 3 formatting (pnpm format) over TypeScript and Markdown files

Files:

  • apps/marketing/src/components/FeatureCardPlain.tsx
  • apps/marketing/src/components/FeatureCard.tsx
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Name React component files in PascalCase (e.g., AppSideBar.tsx)

Files:

  • apps/marketing/src/components/FeatureCardPlain.tsx
  • apps/marketing/src/components/FeatureCard.tsx
🔇 Additional comments (3)
apps/marketing/src/components/FeatureCardPlain.tsx (1)

10-23: LGTM on structure and styling.

Layout, tokens, and placeholders are consistent with the design system.

apps/marketing/src/components/FeatureCard.tsx (2)

3-3: Imports look correct.

next/image usage and types align with Next.js patterns.


28-41: Default hero images verified — no action required.

</div>

<div className="p-5 flex-1 flex flex-col">
<h3 className="text-base sm:text-lg text-primary font-sans">{title || ""}</h3>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid empty heading; render title conditionally.

Same a11y concern as the plain card.

-          <h3 className="text-base sm:text-lg text-primary font-sans">{title || ""}</h3>
+          {title ? (
+            <h3 className="text-base sm:text-lg text-primary font-sans">{title}</h3>
+          ) : (
+            <div className="h-[1.25rem]" aria-hidden="true"></div>
+          )}
📝 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
<h3 className="text-base sm:text-lg text-primary font-sans">{title || ""}</h3>
{title ? (
<h3 className="text-base sm:text-lg text-primary font-sans">{title}</h3>
) : (
<div className="h-[1.25rem]" aria-hidden="true"></div>
)}
🤖 Prompt for AI Agents
In apps/marketing/src/components/FeatureCard.tsx around line 47, the h3 renders
an empty string when title is falsy which creates an empty heading (a11y issue);
change to render the h3 only when title is truthy by wrapping the element in a
conditional (or return null/alternative content) so no empty heading appears,
and ensure any styling/structure adapts when title is absent.

<div className="h-full rounded-2xl bg-primary/30 p-1 sm:p-1 border-2 border-primary/30 shadow-sm">
<div className="bg-background rounded-xl h-full flex flex-col">
<div className="p-5 flex-1 flex flex-col">
<h3 className="text-base sm:text-lg text-primary font-sans">{title || ""}</h3>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid empty heading; render title conditionally.

An empty

harms a11y. Only render the heading when a title exists; otherwise render a spacer marked decorative.

-          <h3 className="text-base sm:text-lg text-primary font-sans">{title || ""}</h3>
+          {title ? (
+            <h3 className="text-base sm:text-lg text-primary font-sans">{title}</h3>
+          ) : (
+            <div className="h-[1.25rem]" aria-hidden="true"></div>
+          )}
📝 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
<h3 className="text-base sm:text-lg text-primary font-sans">{title || ""}</h3>
{title ? (
<h3 className="text-base sm:text-lg text-primary font-sans">{title}</h3>
) : (
<div className="h-[1.25rem]" aria-hidden="true"></div>
)}
🤖 Prompt for AI Agents
In apps/marketing/src/components/FeatureCardPlain.tsx around line 14, the <h3>
is rendered even when title is empty which creates an empty heading and harms
accessibility; update the component to conditionally render the <h3> only when
title is a non-empty string, and when title is missing render a decorative
spacer element (e.g., a <div> or <span> with aria-hidden="true" and equivalent
styling) so visual layout is preserved without exposing an empty heading to
assistive tech.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/ui/package.json (1)

14-29: Add React as peer dependency (avoid duplicate Reacts in apps)

UI packages should declare react/react-dom as peerDependencies (keep them in devDependencies for local build).

 {
   "devDependencies": {
     "react": "19.1.0",
+    "react-dom": "19.1.0",
   },
+  "peerDependencies": {
+    "react": "^18 || ^19",
+    "react-dom": "^18 || ^19"
+  },
+  "peerDependenciesMeta": {
+    "react-dom": { "optional": false }
+  },
🧹 Nitpick comments (7)
packages/ui/package.json (1)

30-62: Ensure Node engine compatibility for Shiki v3

Shiki v3 is ESM and targets modern Node. Declare engines to prevent accidental installs on unsupported Node.

   "dependencies": {
     "shiki": "^3.3.0",
   },
+  "engines": {
+    "node": ">=18.17"
+  }
packages/ui/src/code-block.tsx (2)

13-36: Type the async return for better DX

Explicit Promise return reduces inference drift and narrows casts.

-export async function CodeBlock(props: Props) {
+export async function CodeBlock(props: Props): Promise<React.JSX.Element> {
   const out = await codeToHast(props.children, {

27-34: Preserve caller classes last in merge

Place user className last to let callers override defaults.

-          className={cn(nodeProps.className, props.className)}
+          className={cn(nodeProps.className, props.className)}

Note: If cn already resolves order predictably, ignore. Otherwise consider cn(props.className, nodeProps.className).

apps/marketing/src/app/page.tsx (4)

86-104: Hero image loading: prefer priority over loading="eager"

Use priority for above-the-fold and drop loading. Duplicate priority false + eager is conflicting.

-                loading="eager"
-                priority={false}
+                priority
-                loading="eager"
-                priority={false}
+                priority

291-316: Copy nits in features (tone/grammar)

Tighten phrasing.

- "Design beautiful campaigns without code using a visual, notion like WYSIWYG editor
+ "Design beautiful campaigns without code using a visual, Notion‑like WYSIWYG editor
...
- "Drop-in SMTP relay that works with any app or framework. Do not get vendor lock-in. Comes in handy with services like Supabase"
+ "Drop‑in SMTP relay that works with any app or framework. Avoid vendor lock‑in. Works great with services like Supabase."

435-436: Sentence case for headline consistency

Match other headings.

-            pay for what you use, not for storing contacts
+            Pay for what you use, not for storing contacts

1-24: Minor DX: import from package entry instead of deep path

Prefer @usesend/ui re-exports to avoid coupling to internal paths.

-import { Button } from "@usesend/ui/src/button";
+import { Button } from "@usesend/ui";
-import { CodeBlock } from "@usesend/ui/src/code-block";
+import { CodeBlock } from "@usesend/ui";

Confirm these are re-exported in packages/ui/index.ts.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cdb7b50 and d5ddc34.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • apps/marketing/src/app/layout.tsx (2 hunks)
  • apps/marketing/src/app/page.tsx (1 hunks)
  • apps/marketing/src/components/FeatureCard.tsx (1 hunks)
  • apps/marketing/src/components/FeatureCardPlain.tsx (1 hunks)
  • packages/ui/code-theme.ts (0 hunks)
  • packages/ui/package.json (1 hunks)
  • packages/ui/src/code-block.tsx (1 hunks)
  • packages/ui/src/code.tsx (0 hunks)
  • packages/ui/styles/globals.css (5 hunks)
💤 Files with no reviewable changes (2)
  • packages/ui/code-theme.ts
  • packages/ui/src/code.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/marketing/src/components/FeatureCard.tsx
  • apps/marketing/src/components/FeatureCardPlain.tsx
  • apps/marketing/src/app/layout.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports, and ensure proper naming of key components.

Files:

  • packages/ui/src/code-block.tsx
  • apps/marketing/src/app/page.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Prefer TypeScript for implementation files (TypeScript-first)
ESLint must pass with zero warnings using @usesend/eslint-config before PRs

Files:

  • packages/ui/src/code-block.tsx
  • apps/marketing/src/app/page.tsx
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,md}: Use 2-space indentation and keep semicolons as enforced by Prettier 3
Run Prettier 3 formatting (pnpm format) over TypeScript and Markdown files

Files:

  • packages/ui/src/code-block.tsx
  • apps/marketing/src/app/page.tsx
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Name React component files in PascalCase (e.g., AppSideBar.tsx)

Files:

  • packages/ui/src/code-block.tsx
  • apps/marketing/src/app/page.tsx
🧠 Learnings (1)
📚 Learning: 2025-09-02T22:22:51.538Z
Learnt from: CR
PR: unsend-dev/unsend#0
File: AGENTS.md:0-0
Timestamp: 2025-09-02T22:22:51.538Z
Learning: Applies to **/*.{ts,tsx} : ESLint must pass with zero warnings using usesend/eslint-config before PRs

Applied to files:

  • packages/ui/package.json
🧬 Code graph analysis (1)
apps/marketing/src/app/page.tsx (4)
apps/marketing/src/components/GitHubStarsButton.tsx (1)
  • GitHubStarsButton (26-69)
apps/marketing/src/components/FeatureCard.tsx (1)
  • FeatureCard (5-63)
apps/marketing/src/components/FeatureCardPlain.tsx (1)
  • FeatureCardPlain (3-26)
packages/ui/src/code-block.tsx (1)
  • CodeBlock (13-36)
🪛 Biome (2.1.2)
apps/marketing/src/app/page.tsx

[error] 385-386: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

🔇 Additional comments (2)
packages/ui/styles/globals.css (1)

17-17: Verify new color tokens meet contrast targets

Primary/primary-light changes affect text and UI affordances; confirm WCAG AA for typical backgrounds.

I can provide a quick script to compute contrast ratios across tokens if helpful.

Also applies to: 79-79, 66-66, 126-126

apps/marketing/src/app/page.tsx (1)

9-11: Verify README anchor (likely wrong)

GitHub usually generates #getting-started, not #-getting-started. The current link may 404.

-const GET_STARTED_URL = `${REPO_URL}#-getting-started`;
+const GET_STARTED_URL = `${REPO_URL}#getting-started`;

Comment thread apps/marketing/src/app/page.tsx
Comment on lines +219 to +224
src={t.image}
alt={`${t.author} avatar`}
width={32}
height={32}
className=" rounded-md border-2 border-primary/50"
/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Next/Image remote domains config required

pbs.twimg.com and cdn.doras.to must be allowed in apps/marketing/next.config.(js|ts), or build will fail.

If missing, add:

// apps/marketing/next.config.ts
export default {
  images: {
    remotePatterns: [
      { protocol: "https", hostname: "pbs.twimg.com" },
      { protocol: "https", hostname: "cdn.doras.to" },
    ],
  },
};

Also applies to: 253-258


🏁 Script executed:

#!/bin/bash
fd -a 'next.config.*' apps/marketing | xargs -I{} sh -c 'echo "== {} =="; cat "{}"'
rg -nP 'images\s*:\s*\{[^}]*\}' apps/marketing -C3

Length of output: 429


Add required Next/Image remotePatterns to next.config.js

In apps/marketing/next.config.js, extend the existing images config to allow external domains:

 images: {
   // Required for static export if using images
   unoptimized: true,
+  remotePatterns: [
+    { protocol: 'https', hostname: 'pbs.twimg.com' },
+    { protocol: 'https', hostname: 'cdn.doras.to' },
+  ],
 },
📝 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
src={t.image}
alt={`${t.author} avatar`}
width={32}
height={32}
className=" rounded-md border-2 border-primary/50"
/>
// apps/marketing/next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// ... other config options
images: {
// Required for static export if using images
unoptimized: true,
remotePatterns: [
{ protocol: 'https', hostname: 'pbs.twimg.com' },
{ protocol: 'https', hostname: 'cdn.doras.to' },
],
},
// ... other config options
};
module.exports = nextConfig;
🤖 Prompt for AI Agents
In apps/marketing/src/app/page.tsx around lines 219 to 224, the Next/Image usage
loads external avatar URLs (t.image) but apps/marketing/next.config.js currently
doesn't allow those remote sources; update apps/marketing/next.config.js to
extend the existing images config by adding either a domains array containing
the external host(s) used for t.image or a remotePatterns entry that matches the
hostname/path of those avatar URLs, merge this into the exported config
(preserving other settings) and restart the dev server so Next.js accepts the
external images.

Comment on lines +383 to +387
<CodeBlock
lang="typescript"
children={code}
className="p-4 rounded-[10px]"
/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix ESLint: don’t pass children via prop

This trips noChildrenProp. Use JSX children.

-                <CodeBlock
-                  lang="typescript"
-                  children={code}
-                  className="p-4 rounded-[10px]"
-                />
+                <CodeBlock lang="typescript" className="p-4 rounded-[10px]">
+                  {code}
+                </CodeBlock>
📝 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
<CodeBlock
lang="typescript"
children={code}
className="p-4 rounded-[10px]"
/>
<CodeBlock lang="typescript" className="p-4 rounded-[10px]">
{code}
</CodeBlock>
🧰 Tools
🪛 Biome (2.1.2)

[error] 385-386: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

🤖 Prompt for AI Agents
In apps/marketing/src/app/page.tsx around lines 383 to 387, the CodeBlock is
passing its content via the children prop which triggers the noChildrenProp
ESLint rule; change the component to use JSX children instead (remove
children={code} and place {code} between the opening and closing <CodeBlock>
tags), preserving the existing lang and className props.

Comment thread packages/ui/package.json
"input-otp": "^1.4.2",
"lucide-react": "^0.503.0",
"next-themes": "^0.4.6",
"pnpm": "^10.9.0",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove pnpm from runtime dependencies

pnpm is a tooling dependency; shipping it in "dependencies" bloats consumers and can break installs. Move it to devDependencies.

Apply:

   "dependencies": {
-    "pnpm": "^10.9.0",
   },
+  "devDependencies": {
+    "pnpm": "^10.9.0",
+  },
📝 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
"pnpm": "^10.9.0",
"dependencies": {
},
"devDependencies": {
"pnpm": "^10.9.0",
},
🤖 Prompt for AI Agents
In packages/ui/package.json around line 54, the pnpm entry is incorrectly listed
under "dependencies"; remove the "pnpm": "^10.9.0" entry from dependencies and
add the same entry to "devDependencies" instead (preserve the version), then
update install artifacts (lockfile / node_modules) by running your package
manager restore so the change is reflected.

Comment on lines +142 to +152
@media (prefers-color-scheme: dark) {
.shiki,
.shiki span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
/* Optional, if you also want font styles */
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use .dark class instead of prefers-color-scheme to match next-themes

Current block follows OS, not the app’s theme toggle. Switch to .dark and add a light default to prevent mismatch.

-@media (prefers-color-scheme: dark) {
-  .shiki,
-  .shiki span {
-    color: var(--shiki-dark) !important;
-    background-color: var(--shiki-dark-bg) !important;
-    /* Optional, if you also want font styles */
-    font-style: var(--shiki-dark-font-style) !important;
-    font-weight: var(--shiki-dark-font-weight) !important;
-    text-decoration: var(--shiki-dark-text-decoration) !important;
-  }
-}
+/* Shiki multi-theme (light default) */
+:where(.shiki, .shiki span) {
+  color: var(--shiki-light) !important;
+  background-color: var(--shiki-light-bg) !important;
+  font-style: var(--shiki-light-font-style) !important;
+  font-weight: var(--shiki-light-font-weight) !important;
+  text-decoration: var(--shiki-light-text-decoration) !important;
+}
+.dark :where(.shiki, .shiki span) {
+  color: var(--shiki-dark) !important;
+  background-color: var(--shiki-dark-bg) !important;
+  font-style: var(--shiki-dark-font-style) !important;
+  font-weight: var(--shiki-dark-font-weight) !important;
+  text-decoration: var(--shiki-dark-text-decoration) !important;
+}
🤖 Prompt for AI Agents
In packages/ui/styles/globals.css around lines 142 to 152, the media query uses
prefers-color-scheme: dark which follows the OS setting instead of the
app-controlled theme; replace the @media block with a .dark selector so styles
apply when the app adds the dark class, and also add corresponding default
(light) variables/rules outside the .dark block to ensure light-mode styles are
explicit to avoid mismatches when the theme toggle is used.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (3)
apps/marketing/src/app/page.tsx (3)

47-51: Fix nested interactive elements: use Button’s asChild

Same a11y issue as noted previously—replace with asChild.

-          <Button size="lg" className="px-6">
-            <a href={APP_URL} target="_blank" rel="noopener noreferrer">
-              Get started
-            </a>
-          </Button>
+          <Button asChild size="lg" className="px-6">
+            <a href={APP_URL} target="_blank" rel="noopener noreferrer">
+              Get started
+            </a>
+          </Button>
-          <Button size="lg" className="px-6">
-            <a
-              href="https://docs.usesend.com"
-              target="_blank"
-              rel="noopener noreferrer"
-            >
-              Read the docs
-            </a>
-          </Button>
+          <Button asChild size="lg" className="px-6">
+            <a
+              href="https://docs.usesend.com"
+              target="_blank"
+              rel="noopener noreferrer"
+            >
+              Read the docs
+            </a>
+          </Button>
-            <Button className="">
-              <a
-                href="https://app.usesend.com"
-                target="_blank"
-                rel="noopener noreferrer"
-              >
-                Get started
-              </a>
-            </Button>
+            <Button asChild>
+              <a
+                href="https://app.usesend.com"
+                target="_blank"
+                rel="noopener noreferrer"
+              >
+                Get started
+              </a>
+            </Button>

Also applies to: 332-341, 420-428


156-162: Next/Image remote domains config needed for external avatars

Allow pbs.twimg.com and cdn.doras.to in apps/marketing/next.config.(js|ts) images.remotePatterns, or Next build will fail.

Also applies to: 190-196


321-325: Fix lint error: don’t pass children via prop

Change CodeBlock to use JSX children (Biome noChildrenProp).

-                <CodeBlock
-                  lang="typescript"
-                  children={code}
-                  className="p-4 rounded-[10px]"
-                />
+                <CodeBlock lang="typescript" className="p-4 rounded-[10px]">
+                  {code}
+                </CodeBlock>
🧹 Nitpick comments (2)
apps/marketing/src/components/TopNav.tsx (1)

8-11: Deduplicate APP/REPO constants

TopNav defines REPO_URL/APP_URL while page.tsx defines the same. Prefer a shared constants module or pass as props from the page to avoid drift.

apps/marketing/src/app/page.tsx (1)

231-233: Minor copy edits (optional, marketing polish)

Tighten phrasing and capitalization.

-      title: "Marketing Email Editor",
+      title: "Marketing Email Editor",
@@
-        "Design beautiful campaigns without code using a visual, notion like WYSIWYG editor that works in major email clients. Reuse templates and brand styles, and personalize with variables.",
+        "Design beautiful campaigns without code using a visual, Notion‑like WYSIWYG editor that works in major email clients. Reuse templates and brand styles, and personalize with variables.",
@@
-        "Drop-in SMTP relay that works with any app or framework. Do not get vendor lock-in. Comes in handy with services like Supabase",
+        "Drop‑in SMTP relay that works with any app or framework. Avoid vendor lock‑in. Works well with services like Supabase.",
@@
-            pay for what you use, not for storing contacts
+            Pay for what you use, not for storing contacts

Also applies to: 254-255, 373-374

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d5ddc34 and 4a94c47.

📒 Files selected for processing (4)
  • apps/marketing/src/app/page.tsx (1 hunks)
  • apps/marketing/src/components/FeatureCard.tsx (1 hunks)
  • apps/marketing/src/components/FeatureCardPlain.tsx (1 hunks)
  • apps/marketing/src/components/TopNav.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/marketing/src/components/FeatureCardPlain.tsx
  • apps/marketing/src/components/FeatureCard.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports, and ensure proper naming of key components.

Files:

  • apps/marketing/src/components/TopNav.tsx
  • apps/marketing/src/app/page.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Prefer TypeScript for implementation files (TypeScript-first)
ESLint must pass with zero warnings using @usesend/eslint-config before PRs

Files:

  • apps/marketing/src/components/TopNav.tsx
  • apps/marketing/src/app/page.tsx
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,md}: Use 2-space indentation and keep semicolons as enforced by Prettier 3
Run Prettier 3 formatting (pnpm format) over TypeScript and Markdown files

Files:

  • apps/marketing/src/components/TopNav.tsx
  • apps/marketing/src/app/page.tsx
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Name React component files in PascalCase (e.g., AppSideBar.tsx)

Files:

  • apps/marketing/src/components/TopNav.tsx
  • apps/marketing/src/app/page.tsx
🧬 Code graph analysis (2)
apps/marketing/src/components/TopNav.tsx (1)
packages/email-editor/src/renderer.tsx (1)
  • button (547-593)
apps/marketing/src/app/page.tsx (5)
apps/marketing/src/components/TopNav.tsx (1)
  • TopNav (12-104)
apps/marketing/src/components/GitHubStarsButton.tsx (1)
  • GitHubStarsButton (26-69)
apps/marketing/src/components/FeatureCard.tsx (1)
  • FeatureCard (5-61)
apps/marketing/src/components/FeatureCardPlain.tsx (1)
  • FeatureCardPlain (3-28)
packages/ui/src/code-block.tsx (1)
  • CodeBlock (13-36)
🪛 Biome (2.1.2)
apps/marketing/src/app/page.tsx

[error] 457-458: Avoid using target="_blank" without rel="noopener" or rel="noreferrer".

Opening external links in new tabs without rel="noopener" is a security risk. See the explanation for more details.
Safe fix: Add the rel="noopener" attribute.

(lint/security/noBlankTarget)


[error] 323-324: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

🔇 Additional comments (1)
apps/marketing/src/app/page.tsx (1)

16-25: Structure/readability LGTM

The modular sections and semantic layout look good.

import Image from "next/image";
import Link from "next/link";
import { GitHubStarsButton } from "~/components/GitHubStarsButton";
import { Button } from "@usesend/ui/src/button";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Avoid deep imports from ui package

Use public exports for Button and CodeBlock.

-import { Button } from "@usesend/ui/src/button";
+import { Button } from "@usesend/ui/button";
@@
-import { CodeBlock } from "@usesend/ui/src/code-block";
+import { CodeBlock } from "@usesend/ui/code-block";

Verify exports:

Also applies to: 8-8


🏁 Script executed:

#!/bin/bash
jq -r '.exports' packages/ui/package.json || cat packages/ui/package.json

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Verify UI package entrypoints and component file paths

# 1. Show main/module entrypoints
jq -r '.main, .module' packages/ui/package.json

# 2. List top-level source files
echo "Top-level files in packages/ui/src:"
ls -1 packages/ui/src

# 3. Locate Button and CodeBlock implementations
echo "Button component files:"
find packages/ui/src -maxdepth 2 -type f \( -iname "button*.{ts,tsx,js,jsx}" \)
echo "CodeBlock component files:"
find packages/ui/src -maxdepth 2 -type f \( -iname "code-block*.{ts,tsx,js,jsx}" \)

Length of output: 855


🏁 Script executed:

#!/bin/bash
# Locate and inspect index.ts in the ui package

echo "Index.ts files in packages/ui:"
find packages/ui -type f -iname "index.ts"

echo -e "\n--- Content of packages/ui/index.ts ---"
sed -n '1,200p' packages/ui/index.ts

Length of output: 424


🏁 Script executed:

#!/bin/bash
# Show full package.json of the UI package to inspect subpath exports
sed -n '1,200p' packages/ui/package.json

Length of output: 1865


Add public exports for Button and CodeBlock before switching to root imports
In packages/ui/index.ts, export the components:

export { Button } from "./src/button";
export { CodeBlock } from "./src/code-block";

Then update imports in apps/marketing/src/app/page.tsx (lines 4 & 8):

-import { Button } from "@usesend/ui/src/button";
+import { Button } from "@usesend/ui";

-import { CodeBlock } from "@usesend/ui/src/code-block";
+import { CodeBlock } from "@usesend/ui";
🤖 Prompt for AI Agents
In apps/marketing/src/app/page.tsx around line 4 (and also update line 8), the
file currently imports Button (and CodeBlock) using deep root paths; add public
exports in packages/ui/index.ts by exporting Button and CodeBlock from their src
modules (export { Button } from "./src/button"; export { CodeBlock } from
"./src/code-block";), then update the imports in page.tsx to import from the
package root (e.g. import { Button, CodeBlock } from "@usesend/ui") instead of
the long @usesend/ui/src/... paths.

Comment on lines +166 to +174
href={`https://${t.company}`}
target="_blank"
className="text-muted-foreground hover:text-primary-light"
>
{" "}
— {t.company}
</a>{" "}
</figcaption>
</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add rel="noopener noreferrer" to external links with target="_blank"

Security best practice (and flagged by Biome).

-                  <a
+                  <a
                     href={`https://${t.company}`}
                     target="_blank"
+                    rel="noopener noreferrer"
                     className="text-muted-foreground hover:text-primary-light"
                   >
-                  <a
+                  <a
                     href={`https://${t.company}`}
                     target="_blank"
+                    rel="noopener noreferrer"
                     className="text-muted-foreground hover:text-primary-light"
                   >
-            <a
+            <a
               href="https://github.com/sponsors/KMKoushik"
               target="_blank"
+              rel="noopener noreferrer"
               className="text-primary-light"
             >

Also applies to: 200-207, 456-459

🤖 Prompt for AI Agents
In apps/marketing/src/app/page.tsx around lines 166-174 (and also at 200-207 and
456-459), external anchor tags use target="_blank" but are missing rel="noopener
noreferrer"; update each external <a> to include rel="noopener noreferrer"
alongside target="_blank" to prevent reverse tabnabbing and ensure security and
lint compliance.

import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { Button } from "@usesend/ui/src/button";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Avoid deep imports from ui package

Import Button via public exports to prevent bundling/TS path issues.

-import { Button } from "@usesend/ui/src/button";
+import { Button } from "@usesend/ui/button";

Run to verify available exports:


🏁 Script executed:

#!/bin/bash
jq -r '.exports' packages/ui/package.json || cat packages/ui/package.json

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Show package.json to inspect main/exports configuration
echo "-- package.json (first 50 lines) --"
head -n 50 packages/ui/package.json

# List the package directory structure
echo "-- packages/ui file tree --"
find packages/ui -maxdepth 4 -type f | sed 's/^/  /'

# Locate all files related to Button component
echo "-- Button-related files in packages/ui --"
find packages/ui -type f | grep -i button

Length of output: 3146


🏁 Script executed:

#!/bin/bash
echo "-- packages/ui/index.ts --"
head -n 200 packages/ui/index.ts

echo ""
echo "== tsconfig files =="
find . -maxdepth 2 -type f -name 'tsconfig*.json' | while read f; do
  echo "---- $f ----"
  head -n 200 "$f"
  echo ""
done

Length of output: 545


Export Button in public API before updating imports
Add export { Button } from "./src/button"; to packages/ui/index.ts, then switch your import to:

-import { Button } from "@usesend/ui/src/button";
+import { Button } from "@usesend/ui";
📝 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 { Button } from "@usesend/ui/src/button";
import { Button } from "@usesend/ui";
🤖 Prompt for AI Agents
In apps/marketing/src/components/TopNav.tsx around line 6, the file imports
Button directly from the internal path "@usesend/ui/src/button"; instead export
Button from the UI package public API by adding an export line for Button in
packages/ui/index.ts (e.g. export the Button from ./src/button), run the package
build/type-check to ensure the export is visible, then update this file to
import Button from "@usesend/ui" instead of the internal src path.

Comment on lines +44 to +48
<Button size="sm" className="ml-2">
<a href={APP_URL} target="_blank" rel="noopener noreferrer">
Get started
</a>
</Button>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix nested interactive elements: render via Button’s asChild

Avoid inside . Use the Radix Slot pattern to keep valid, accessible markup.

-          <Button size="sm" className="ml-2">
-            <a href={APP_URL} target="_blank" rel="noopener noreferrer">
-              Get started
-            </a>
-          </Button>
+          <Button asChild size="sm" className="ml-2">
+            <a href={APP_URL} target="_blank" rel="noopener noreferrer">
+              Get started
+            </a>
+          </Button>
-            <div className="pt-2">
-              <Button className="w-full">
-                <a href={APP_URL} target="_blank" rel="noopener noreferrer" onClick={() => setOpen(false)}>
-                  Get started
-                </a>
-              </Button>
-            </div>
+            <div className="pt-2">
+              <Button asChild className="w-full">
+                <a href={APP_URL} target="_blank" rel="noopener noreferrer" onClick={() => setOpen(false)}>
+                  Get started
+                </a>
+              </Button>
+            </div>

Also applies to: 93-97

🤖 Prompt for AI Agents
In apps/marketing/src/components/TopNav.tsx around lines 44-48 (and also apply
the same change to lines 93-97), currently an anchor is nested inside a Button
causing nested interactive elements; instead enable the Button's asChild prop
and render the anchor as the Button's child so the Button delegates to the
anchor (preserving accessibility and valid DOM). Update the Button usage to
include asChild and move the anchor inside as the child element with the href,
target, and rel attributes so the output is a single interactive element.

Comment on lines +52 to +57
<button
aria-label="Open menu"
className="sm:hidden inline-flex items-center justify-center rounded-md p-2 text-muted-foreground hover:text-foreground hover:bg-accent focus:outline-none focus:ring-2 focus:ring-border"
onClick={() => setOpen((v) => !v)}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="h-6 w-6">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add ARIA state and panel wiring for the mobile menu

Expose state to ATs and link the button to the panel.

-        <button
-          aria-label="Open menu"
+        <button
+          aria-label={open ? "Close menu" : "Open menu"}
+          aria-expanded={open}
+          aria-controls="mobile-menu"
           className="sm:hidden inline-flex items-center justify-center rounded-md p-2 text-muted-foreground hover:text-foreground hover:bg-accent focus:outline-none focus:ring-2 focus:ring-border"
           onClick={() => setOpen((v) => !v)}
         >
-        <div className="sm:hidden border-t border-border bg-sidebar-background/95 backdrop-blur">
+        <div id="mobile-menu" role="navigation" aria-label="Primary" className="sm:hidden border-t border-border bg-sidebar-background/95 backdrop-blur">

Also applies to: 69-71

🤖 Prompt for AI Agents
In apps/marketing/src/components/TopNav.tsx around lines 52-57 (and also apply
same change to the button at 69-71), the mobile menu button toggles state but
does not expose ARIA state or link to the panel; add aria-expanded={open} and
aria-controls="mobile-menu" to the button (so ATs know when it’s open) and give
the mobile menu panel an id="mobile-menu" and appropriate aria-hidden={!open} or
role="menu" as needed; ensure the panel and button IDs match and the
aria-expanded value is derived from the component open state.

@KMKoushik KMKoushik merged commit 3e6d4d1 into main Sep 5, 2025
2 of 4 checks passed
@KMKoushik KMKoushik deleted the km/2025-09-04-rebrand-landing branch September 5, 2025 12:32
@coderabbitai coderabbitai bot mentioned this pull request Sep 6, 2025
KMKoushik added a commit that referenced this pull request Sep 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant