Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .cursor/rules/read-rules-and-skills-first.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
description: Forces the agent to read all applicable rules and skills before writing any code
alwaysApply: true
---

# Read Rules & Skills Before Coding

**Before writing or editing any code**, you MUST:

1. **Read all glob-matched rules** in `.cursor/rules/` that apply to the file types you are about to create or edit (e.g. `typescript-javascript-standards.mdc` for `.ts`/`.tsx` files).
2. **Read every skill referenced** by those rules (e.g. if a rule says "Use the /react-component skill", read that skill file immediately).
3. **Only then** begin writing code, ensuring every standard is applied on the first pass.

```
❌ BAD — write code, then retroactively fix standards
1. Create component
2. User points out missing JSDoc / tests / arrow functions
3. Spend extra tokens fixing

✅ GOOD — read rules first, write correct code once
1. Read typescript-javascript-standards.mdc
2. Read react-component skill, vitest-unit-testing skill
3. Create component following all standards (arrow fn, JSDoc, DI, tests)
```
4 changes: 4 additions & 0 deletions .cursor/rules/typescript-javascript-standards.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ test("getActiveUsers returns only active users", async () => {

Use the /react-component skill when creating a React component.

## Email template

Use the /email-template skill when creating an email template.

## React Server Action/Function

Follow the /route-action-gen-workflow rule when creating a server action/function.
Expand Down
105 changes: 105 additions & 0 deletions .cursor/skills/email-template/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
name: email-template
description: Create email templates using React Email in the @workspace/email-templates package. Use when the user asks to create, add, build, or scaffold an email template, transactional email, or mentions React Email, email-templates, or @react-email/components.
---

# Email Template Creation

Create email templates in `packages/email-templates/src/` using `@react-email/components`.

## File Conventions

- Place templates in a **domain subfolder**: `src/user/`, `src/order/`, `src/billing/`, etc.
- File names in **kebab-case**: `password-forgot.tsx`, `order-confirmation.tsx`.
- No `index.tsx` files inside subfolders -- each template is a standalone file.

## Required Exports

Every template file must export exactly these four things:

1. **Props interface** -- named `{ComponentName}Props`.
2. **Named const component** -- arrow function typed as `React.JSX.Element`.
3. **PreviewProps** -- static property on the component with sample data, using `satisfies`.
4. **Default export** -- the component.

```tsx
import {
Body,
Container,
Head,
Heading,
Html,
Preview,
Text,
} from "@react-email/components";

export interface InviteEmailProps {
inviterName: string;
inviteUrl: string;
}

/**
* Email sent when a user is invited to join a workspace.
*
* @param props.inviterName - Name of the person who sent the invite.
* @param props.inviteUrl - URL the recipient clicks to accept.
* @returns The invite email React Email component.
*/
export const InviteEmail = ({
inviterName,
inviteUrl,
}: InviteEmailProps): React.JSX.Element => {
return (
<Html>
<Head />
<Preview>{inviterName} invited you to join</Preview>
<Body style={main}>
<Container style={container}>
<Heading style={heading}>You're invited!</Heading>
<Text style={paragraph}>
{inviterName} invited you to join the workspace.
</Text>
</Container>
</Body>
</Html>
);
};

InviteEmail.PreviewProps = {
inviterName: "Jane",
inviteUrl: "https://example.com/invite?token=abc",
} satisfies InviteEmailProps;

export default InviteEmail;
```

## Styling

- Use plain `React.CSSProperties` objects defined as `const` at the bottom of the file.
- Common styles: `main` (body background + font), `container` (white card), `heading`, `paragraph`, `button`, `buttonContainer`, `hr`, `footer`.
- Match the existing style objects from other templates for visual consistency.

## After Creating a Template

Run the codegen script to regenerate the template registry:

```bash
pnpm --filter @workspace/email-templates generate
```

This updates `src/index.ts` with the new template's entry in `TemplateMap` and `templates`. The generated file must not be edited by hand.

## Checklist

Before finalizing a template:

- [ ] File is in a domain subfolder under `src/` (e.g., `src/user/`)
- [ ] File name is kebab-case
- [ ] Exports an `interface` ending in `Props`
- [ ] Exports a named `const` arrow-function component with JSDoc
- [ ] Component return type is `React.JSX.Element`
- [ ] `PreviewProps` set with `satisfies {PropsType}`
- [ ] Default export of the component
- [ ] Styles are `React.CSSProperties` objects at the bottom
- [ ] Co-located `.test.tsx` with 100% coverage (renders HTML containing key props)
- [ ] Ran `pnpm --filter @workspace/email-templates generate`
54 changes: 54 additions & 0 deletions apps/web/app/email-demo/actions/route.post.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* To test this end point during development, run the following command:
*
* 1. Run "docker-compose up" from the root of the monorepo to start the email server.
* 2. Set the environment variable "NODE_ENV" to "development".
* 3. Run the development server by running "pnpm dev --filter=web" from the root of the monorepo.
* 4. Send a request to the end point using the following command:
* curl -X POST http://localhost:3000/api/email-demo -H "Content-Type: application/json" -d '{"email": "test@example.com", "name": "Test User"}'
*/

import { z } from "zod";
import {
createRequestValidator,
HandlerFunc,
successResponse,
} from "route-action-gen/lib";
import { getEmailSender } from "@/lib/email/smtp";

const bodyValidator = z.object({
email: z.string().email(),
name: z.string().min(1),
});

export const requestValidator = createRequestValidator({
body: bodyValidator,
});

export const responseValidator = z.object({
success: z.boolean(),
});

export const handler: HandlerFunc<
typeof requestValidator,
typeof responseValidator,
undefined
> = async (data) => {
const { body } = data;

const emailSender = getEmailSender();

await emailSender.send({
to: body.email,
subject: "Hello, World!",
template: "user/signup",
data: {
name: body.name,
verifyUrl: "https://example.com/verify?token=123",
},
});

return successResponse({
success: true,
});
};
1 change: 1 addition & 0 deletions apps/web/app/email-demo/actions/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./.generated/route";
34 changes: 34 additions & 0 deletions apps/web/lib/email/smtp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {
createEmailSender,
createResendTransport,
createSmtpTransport,
EmailSender,
} from "@workspace/email-send";
import { env } from "@workspace/env";

let emailSender: EmailSender | null = null;

export const getEmailSender = ({
from = "HyperJump <noreply@hyperjump.com>",
}: {
from?: string;
} = {}): EmailSender => {
if (!emailSender) {
if (env.NODE_ENV === "production" && !env.RESEND_API_KEY) {
throw new Error("RESEND_API_KEY is not set");
}
emailSender = createEmailSender({
from,
transport:
env.NODE_ENV === "production"
? createResendTransport({
apiKey: env.RESEND_API_KEY!,
})
: createSmtpTransport({
host: "localhost",
port: 2500,
}),
});
}
return emailSender;
};
2 changes: 1 addition & 1 deletion apps/web/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"test:coverage": "vitest --run --coverage"
},
"dependencies": {
"@workspace/email-send": "workspace:*",
"@workspace/env": "workspace:*",
"@workspace/ui": "workspace:*",
"@prisma/adapter-pg": "^7.2.0",
Expand Down
7 changes: 7 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,10 @@ services:
volumes:
- ./data:/var/lib/postgresql/data
restart: unless-stopped
inbucket:
image: inbucket/inbucket:latest
ports:
- 9000:9000 # web interface. Open http://localhost:9000 to view the inbox.
- 2500:2500 # SMTP server. Use this to send emails.
- 1100:1100 # POP3 server
restart: unless-stopped
Loading