Conversation
|
Caution Review failedThe pull request is closed. WalkthroughDeletes Changes
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (5)
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
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 whenmetricsis undefined.
metrics?.bounceRate.toFixed(2)will calltoFixedonundefined. 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
isLoadingis true.
245-261: Apply the same guard for complaint rate.Avoid calling
toFixedonundefined.- <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
asChildto 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 avoidInfinity.toFixed()runtime errors.If
sentis 0,percentagebecomesInfinityand.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-primaryis 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 theshapefactory; dropanyand avoidas any.Use props inferred from
Rectangleand a concreteradiustuple.-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,
Teamrows 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 classtext-bluelikely doesn’t exist. Use a tokened color.Switch to
text-primary(ortext-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: preferpriorityfor LCP and droploading="eager".For above‑the‑fold hero, set
priorityand omitloadingto avoid mixed hints.- loading="eager" - priority={false} + priorityApply 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.
⛔ Files ignored due to path filters (2)
apps/marketing/public/hero-dark.pngis excluded by!**/*.pngapps/marketing/public/hero-light.pngis 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.tsxapps/web/src/components/AppSideBar.tsxpackages/ui/src/typography.tsxpackages/tailwind-config/tailwind.config.tsapps/marketing/src/components/GitHubStarsButton.tsxapps/marketing/src/app/layout.tsxapps/marketing/src/app/page.tsxapps/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.tsxapps/web/src/components/AppSideBar.tsxpackages/ui/src/typography.tsxpackages/tailwind-config/tailwind.config.tsapps/marketing/src/components/GitHubStarsButton.tsxapps/marketing/src/app/layout.tsxapps/marketing/src/app/page.tsxapps/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.tsxapps/web/src/components/AppSideBar.tsxpackages/ui/src/typography.tsxpackages/tailwind-config/tailwind.config.tsapps/marketing/src/components/GitHubStarsButton.tsxapps/marketing/src/app/layout.tsxapps/marketing/src/app/page.tsxapps/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.tsxapps/web/src/components/AppSideBar.tsxpackages/ui/src/typography.tsxapps/marketing/src/components/GitHubStarsButton.tsxapps/marketing/src/app/layout.tsxapps/marketing/src/app/page.tsxapps/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.tsxapps/web/src/components/AppSideBar.tsxapps/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: UseasChildonButtonto render links
Button already supports theasChildprop—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
Thenext.config.jsin apps/marketing already setsimages.unoptimized: truefor 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.
| const STACK_ORDER: string[] = [ | ||
| "delivered", | ||
| "bounced", | ||
| "complained", | ||
| "opened", | ||
| "clicked", | ||
| ] as const; | ||
|
|
||
| type StackKey = (typeof STACK_ORDER)[number]; | ||
|
|
There was a problem hiding this comment.
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.
| 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"; |
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this comment.
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-1duplicatesp-1with 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-hiddenon 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 responsivesizesand improvealtsemantics.
- With
fill, setsizesto prevent overserving.- Derive
altfrom 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: Exposepriorityto 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-1duplicatesp-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.
📒 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.tsxapps/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.tsxapps/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.tsxapps/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.tsxapps/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/imageusage 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> |
There was a problem hiding this comment.
🛠️ 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.
| <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> |
There was a problem hiding this comment.
🛠️ 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.
| <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.
There was a problem hiding this comment.
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 v3Shiki 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 DXExplicit 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 mergePlace 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: preferpriorityoverloading="eager"Use
priorityfor above-the-fold and droploading. 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 consistencyMatch 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 pathPrefer
@usesend/uire-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.
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis 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.tsxapps/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.tsxapps/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.tsxapps/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.tsxapps/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 targetsPrimary/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`;
| src={t.image} | ||
| alt={`${t.author} avatar`} | ||
| width={32} | ||
| height={32} | ||
| className=" rounded-md border-2 border-primary/50" | ||
| /> |
There was a problem hiding this comment.
💡 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 -C3Length 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.
| 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.
| <CodeBlock | ||
| lang="typescript" | ||
| children={code} | ||
| className="p-4 rounded-[10px]" | ||
| /> |
There was a problem hiding this comment.
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.
| <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.
| "input-otp": "^1.4.2", | ||
| "lucide-react": "^0.503.0", | ||
| "next-themes": "^0.4.6", | ||
| "pnpm": "^10.9.0", |
There was a problem hiding this comment.
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.
| "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.
| @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; | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (3)
apps/marketing/src/app/page.tsx (3)
47-51: Fix nested interactive elements: use Button’s asChildSame 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 avatarsAllow 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 propChange 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 constantsTopNav 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 contactsAlso 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.
📒 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.tsxapps/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.tsxapps/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.tsxapps/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.tsxapps/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 LGTMThe 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"; |
There was a problem hiding this comment.
💡 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.jsonLength 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.tsLength 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.jsonLength 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.
| href={`https://${t.company}`} | ||
| target="_blank" | ||
| className="text-muted-foreground hover:text-primary-light" | ||
| > | ||
| {" "} | ||
| — {t.company} | ||
| </a>{" "} | ||
| </figcaption> | ||
| </div> |
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
💡 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.jsonLength 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 buttonLength 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 ""
doneLength 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.
| 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.
| <Button size="sm" className="ml-2"> | ||
| <a href={APP_URL} target="_blank" rel="noopener noreferrer"> | ||
| Get started | ||
| </a> | ||
| </Button> |
There was a problem hiding this comment.
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.
| <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"> |
There was a problem hiding this comment.
🛠️ 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.
Summary by CodeRabbit
New Features
Improvements
Style
Chores