feat: footer social links, cancel appointment button, Resend-ready docs

Made-with: Cursor
This commit is contained in:
“kirukib” 2026-03-12 00:53:32 +03:00
parent 2d830fa8d2
commit 6c63b91afd
5 changed files with 163 additions and 15 deletions

View File

@ -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"
/>
),
})
```

View File

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

View File

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

View File

@ -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',
},
},
{

View File

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