feat: footer social links, cancel appointment button, Resend-ready docs
Made-with: Cursor
This commit is contained in:
parent
2d830fa8d2
commit
6c63b91afd
52
README.md
52
README.md
|
|
@ -38,36 +38,70 @@ All templates are wrapped with `BaseEmailShell`, which lives in `src/email/BaseE
|
|||
|
||||
### Using with Resend
|
||||
|
||||
These templates are written as plain React components so they can be adapted to Resend in several ways:
|
||||
These templates are **ready to use with Resend**. They are plain React components that render to static HTML with:
|
||||
|
||||
1. **Copy JSX directly** into a dedicated Resend email project that uses React rendering (e.g. with `@react-email/components` or your own renderer).
|
||||
2. **Render to static HTML** in a Node script (using `react-dom/server`) and paste that HTML into Resend's dashboard as a custom template.
|
||||
The preview app now has an **HTML tab** which shows the full HTML (including `<!DOCTYPE html>`) for the currently selected template so you can copy it directly.
|
||||
3. Use this app only for visual QA, while you keep the production copies of these components in your backend or email-service repo.
|
||||
- **Footer**: Social links (Instagram, X, YouTube, Facebook, LinkedIn), "Powered by Yaltopia Home", and a verification link. Override links via `socialLinks` on `BaseEmailShell`.
|
||||
- **Logo**: Pass `logoUrl` (absolute URL) when sending so the logo loads in email clients (e.g. `https://yaltopia.home/logo.svg`).
|
||||
- **Appointment email**: Includes "Add to calendar" and **"Cancel appointment"** buttons; pass `calendarUrl` and `cancelUrl` from your backend.
|
||||
|
||||
When integrating, replace the sample props in `sampleData.ts` with real data and ensure links (payment, calendar, reset, etc.) are generated by your backend.
|
||||
You can use them in two ways:
|
||||
|
||||
#### Example: sending via Resend with React templates
|
||||
1. **React (recommended)** – Use the `react` option in `resend.emails.send()` and pass the template component with props. Resend will render to HTML.
|
||||
2. **HTML** – Use the **HTML** tab in the preview app to copy the full HTML for a template, then use it as a custom template in Resend (replace placeholders with your variables).
|
||||
|
||||
When integrating, replace sample props with real data and generate all URLs (payment, calendar, cancel, reset) from your backend.
|
||||
|
||||
#### Example: sending via Resend with React
|
||||
|
||||
```ts
|
||||
import { Resend } from 'resend'
|
||||
import { InvitationTeamMemberEmail } from './src/email/templates'
|
||||
import { InvitationTeamMemberEmail, AppointmentBookedEmail } from './src/email/templates'
|
||||
import { BaseEmailShell } from './src/email/BaseEmailShell'
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY)
|
||||
|
||||
// Optional: override footer social links and logo for all emails
|
||||
const sharedShellProps = {
|
||||
socialLinks: {
|
||||
instagram: 'https://instagram.com/youraccount',
|
||||
twitter: 'https://x.com/youraccount',
|
||||
facebook: 'https://facebook.com/youraccount',
|
||||
},
|
||||
logoUrl: 'https://yaltopia.home/assets/logo.svg',
|
||||
}
|
||||
|
||||
await resend.emails.send({
|
||||
from: 'Yaltopia Home <no-reply@yaltopia.home>',
|
||||
to: 'user@example.com',
|
||||
subject: 'You have been invited to Yaltopia Home',
|
||||
react: (
|
||||
<InvitationTeamMemberEmail
|
||||
recipientName="Ricky Ricardo"
|
||||
recipientName="Jane Doe"
|
||||
teamName="Yaltopia Home Ops"
|
||||
role="Leasing Manager"
|
||||
inviteUrl="https://yaltopia.home/invite/abc123"
|
||||
/>
|
||||
),
|
||||
})
|
||||
|
||||
// Appointment with calendar + cancel
|
||||
await resend.emails.send({
|
||||
from: 'Yaltopia Home <no-reply@yaltopia.home>',
|
||||
to: 'user@example.com',
|
||||
subject: 'Your viewing appointment is confirmed',
|
||||
react: (
|
||||
<AppointmentBookedEmail
|
||||
recipientName="Jane Doe"
|
||||
date="February 15, 2026"
|
||||
time="2:00 PM"
|
||||
timezone="EAT"
|
||||
location="123 Property St, Gate A"
|
||||
agentName="Amara, Yaltopia Home"
|
||||
calendarUrl="https://yaltopia.home/calendar/add/apt-123"
|
||||
cancelUrl="https://yaltopia.home/appointments/apt-123/cancel"
|
||||
/>
|
||||
),
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,11 +11,32 @@ export type EmailCategory =
|
|||
| 'appointment'
|
||||
| 'security'
|
||||
|
||||
/** Optional social links for the footer. Omit any you do not use. */
|
||||
export interface SocialLinks {
|
||||
instagram?: string
|
||||
twitter?: string
|
||||
youtube?: string
|
||||
facebook?: string
|
||||
linkedin?: string
|
||||
}
|
||||
|
||||
const DEFAULT_SOCIAL_LINKS: SocialLinks = {
|
||||
instagram: 'https://instagram.com/yaltopiahome',
|
||||
twitter: 'https://x.com/yaltopiahome',
|
||||
youtube: 'https://youtube.com/@yaltopiahome',
|
||||
facebook: 'https://facebook.com/yaltopiahome',
|
||||
linkedin: 'https://linkedin.com/company/yaltopiahome',
|
||||
}
|
||||
|
||||
interface BaseEmailShellProps {
|
||||
title: string
|
||||
eyebrow?: string
|
||||
category: EmailCategory
|
||||
children: ReactNode
|
||||
/** Override default social links in the footer. Pass from Resend with your real URLs. */
|
||||
socialLinks?: SocialLinks
|
||||
/** Full URL to the logo image. For Resend, use an absolute URL (e.g. https://yaltopia.home/logo.svg). */
|
||||
logoUrl?: string
|
||||
}
|
||||
|
||||
const categoryLabel: Record<EmailCategory, string> = {
|
||||
|
|
@ -34,7 +55,17 @@ export function BaseEmailShell({
|
|||
eyebrow,
|
||||
category,
|
||||
children,
|
||||
socialLinks: socialLinksProp,
|
||||
logoUrl = '/YaltopiaHomesLogo.svg',
|
||||
}: BaseEmailShellProps) {
|
||||
const links = { ...DEFAULT_SOCIAL_LINKS, ...socialLinksProp }
|
||||
const hasSocial =
|
||||
links.instagram ||
|
||||
links.twitter ||
|
||||
links.youtube ||
|
||||
links.facebook ||
|
||||
links.linkedin
|
||||
|
||||
return (
|
||||
<div className="email-root">
|
||||
<div className="email-shell">
|
||||
|
|
@ -42,7 +73,7 @@ export function BaseEmailShell({
|
|||
<div className="brand-lockup">
|
||||
<div className="brand-mark">
|
||||
<img
|
||||
src="/YaltopiaHomesLogo.svg"
|
||||
src={logoUrl}
|
||||
alt="Yaltopia Home"
|
||||
className="brand-logo"
|
||||
/>
|
||||
|
|
@ -57,11 +88,40 @@ export function BaseEmailShell({
|
|||
{children}
|
||||
</main>
|
||||
<footer className="email-footer">
|
||||
<div>
|
||||
<div className="footer-left">
|
||||
<div className="footer-brand">Yaltopia Home</div>
|
||||
<div className="footer-meta">
|
||||
Transactional email · Please do not reply to this address.
|
||||
</div>
|
||||
{hasSocial && (
|
||||
<div className="footer-social">
|
||||
{links.instagram && (
|
||||
<a href={links.instagram} className="footer-social-link" target="_blank" rel="noopener noreferrer">
|
||||
Instagram
|
||||
</a>
|
||||
)}
|
||||
{links.twitter && (
|
||||
<a href={links.twitter} className="footer-social-link" target="_blank" rel="noopener noreferrer">
|
||||
X
|
||||
</a>
|
||||
)}
|
||||
{links.youtube && (
|
||||
<a href={links.youtube} className="footer-social-link" target="_blank" rel="noopener noreferrer">
|
||||
YouTube
|
||||
</a>
|
||||
)}
|
||||
{links.facebook && (
|
||||
<a href={links.facebook} className="footer-social-link" target="_blank" rel="noopener noreferrer">
|
||||
Facebook
|
||||
</a>
|
||||
)}
|
||||
{links.linkedin && (
|
||||
<a href={links.linkedin} className="footer-social-link" target="_blank" rel="noopener noreferrer">
|
||||
LinkedIn
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="footer-powered">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -174,6 +174,27 @@ body {
|
|||
color: #111827;
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
display: inline-block;
|
||||
padding: 10px 18px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
background: transparent;
|
||||
color: #374151;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.secondary-button:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.button-row--secondary {
|
||||
margin-top: -8px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||
margin: 16px 0 18px;
|
||||
|
|
@ -185,9 +206,16 @@ body {
|
|||
background: #050505;
|
||||
color: #f9fafb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.footer-brand {
|
||||
|
|
@ -201,9 +229,28 @@ body {
|
|||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.footer-social {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.footer-social-link {
|
||||
font-size: 11px;
|
||||
color: #9ca3af;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer-social-link:hover {
|
||||
color: #e5e7eb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.footer-powered {
|
||||
font-size: 11px;
|
||||
color: #9ca3af;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.footer-approved {
|
||||
|
|
|
|||
|
|
@ -244,6 +244,7 @@ export const templates = [
|
|||
location: '203 Yaltopia Crescent, Gate B',
|
||||
agentName: 'Amara, Yaltopia Home',
|
||||
calendarUrl: 'https://calendar.yaltopia.home/add/apt-0293',
|
||||
cancelUrl: 'https://yaltopia.home/appointments/apt-0293/cancel',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -439,10 +439,12 @@ export interface AppointmentBookedEmailProps extends CommonEmailProps {
|
|||
location: string
|
||||
agentName: string
|
||||
calendarUrl: string
|
||||
/** URL to cancel or manage the appointment (e.g. dashboard or cancel endpoint). */
|
||||
cancelUrl: string
|
||||
}
|
||||
|
||||
export function AppointmentBookedEmail(props: AppointmentBookedEmailProps) {
|
||||
const { recipientName, date, time, timezone, location, agentName, calendarUrl } =
|
||||
const { recipientName, date, time, timezone, location, agentName, calendarUrl, cancelUrl } =
|
||||
props
|
||||
return (
|
||||
<BaseEmailShell
|
||||
|
|
@ -480,9 +482,13 @@ export function AppointmentBookedEmail(props: AppointmentBookedEmailProps) {
|
|||
Add to calendar
|
||||
</a>
|
||||
</div>
|
||||
<div className="button-row button-row--secondary">
|
||||
<a href={cancelUrl} className="secondary-button">
|
||||
Cancel appointment
|
||||
</a>
|
||||
</div>
|
||||
<p className="body-text-muted">
|
||||
Please arrive a few minutes early. You can reschedule or cancel from
|
||||
your dashboard.
|
||||
Please arrive a few minutes early. You can reschedule from your dashboard.
|
||||
</p>
|
||||
</BaseEmailShell>
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user