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 ### 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). - **Footer**: Social links (Instagram, X, YouTube, Facebook, LinkedIn), "Powered by Yaltopia Home", and a verification link. Override links via `socialLinks` on `BaseEmailShell`.
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. - **Logo**: Pass `logoUrl` (absolute URL) when sending so the logo loads in email clients (e.g. `https://yaltopia.home/logo.svg`).
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. - **Appointment email**: Includes "Add to calendar" and **"Cancel appointment"** buttons; pass `calendarUrl` and `cancelUrl` from your backend.
3. Use this app only for visual QA, while you keep the production copies of these components in your backend or email-service repo.
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 ```ts
import { Resend } from 'resend' 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) 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({ await resend.emails.send({
from: 'Yaltopia Home <no-reply@yaltopia.home>', from: 'Yaltopia Home <no-reply@yaltopia.home>',
to: 'user@example.com', to: 'user@example.com',
subject: 'You have been invited to Yaltopia Home', subject: 'You have been invited to Yaltopia Home',
react: ( react: (
<InvitationTeamMemberEmail <InvitationTeamMemberEmail
recipientName="Ricky Ricardo" recipientName="Jane Doe"
teamName="Yaltopia Home Ops" teamName="Yaltopia Home Ops"
role="Leasing Manager" role="Leasing Manager"
inviteUrl="https://yaltopia.home/invite/abc123" 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' | 'appointment'
| 'security' | '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 { interface BaseEmailShellProps {
title: string title: string
eyebrow?: string eyebrow?: string
category: EmailCategory category: EmailCategory
children: ReactNode 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> = { const categoryLabel: Record<EmailCategory, string> = {
@ -34,7 +55,17 @@ export function BaseEmailShell({
eyebrow, eyebrow,
category, category,
children, children,
socialLinks: socialLinksProp,
logoUrl = '/YaltopiaHomesLogo.svg',
}: BaseEmailShellProps) { }: BaseEmailShellProps) {
const links = { ...DEFAULT_SOCIAL_LINKS, ...socialLinksProp }
const hasSocial =
links.instagram ||
links.twitter ||
links.youtube ||
links.facebook ||
links.linkedin
return ( return (
<div className="email-root"> <div className="email-root">
<div className="email-shell"> <div className="email-shell">
@ -42,7 +73,7 @@ export function BaseEmailShell({
<div className="brand-lockup"> <div className="brand-lockup">
<div className="brand-mark"> <div className="brand-mark">
<img <img
src="/YaltopiaHomesLogo.svg" src={logoUrl}
alt="Yaltopia Home" alt="Yaltopia Home"
className="brand-logo" className="brand-logo"
/> />
@ -57,11 +88,40 @@ export function BaseEmailShell({
{children} {children}
</main> </main>
<footer className="email-footer"> <footer className="email-footer">
<div> <div className="footer-left">
<div className="footer-brand">Yaltopia Home</div> <div className="footer-brand">Yaltopia Home</div>
<div className="footer-meta"> <div className="footer-meta">
Transactional email · Please do not reply to this address. Transactional email · Please do not reply to this address.
</div> </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>
<div className="footer-powered"> <div className="footer-powered">
<div> <div>

View File

@ -174,6 +174,27 @@ body {
color: #111827; 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 { .divider {
border-top: 1px solid rgba(0, 0, 0, 0.06); border-top: 1px solid rgba(0, 0, 0, 0.06);
margin: 16px 0 18px; margin: 16px 0 18px;
@ -185,9 +206,16 @@ body {
background: #050505; background: #050505;
color: #f9fafb; color: #f9fafb;
display: flex; display: flex;
align-items: center; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
gap: 16px; gap: 16px;
flex-wrap: wrap;
}
.footer-left {
display: flex;
flex-direction: column;
gap: 6px;
} }
.footer-brand { .footer-brand {
@ -201,9 +229,28 @@ body {
color: #d1d5db; 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 { .footer-powered {
font-size: 11px; font-size: 11px;
color: #9ca3af; color: #9ca3af;
text-align: right;
} }
.footer-approved { .footer-approved {

View File

@ -244,6 +244,7 @@ export const templates = [
location: '203 Yaltopia Crescent, Gate B', location: '203 Yaltopia Crescent, Gate B',
agentName: 'Amara, Yaltopia Home', agentName: 'Amara, Yaltopia Home',
calendarUrl: 'https://calendar.yaltopia.home/add/apt-0293', 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 location: string
agentName: string agentName: string
calendarUrl: string calendarUrl: string
/** URL to cancel or manage the appointment (e.g. dashboard or cancel endpoint). */
cancelUrl: string
} }
export function AppointmentBookedEmail(props: AppointmentBookedEmailProps) { export function AppointmentBookedEmail(props: AppointmentBookedEmailProps) {
const { recipientName, date, time, timezone, location, agentName, calendarUrl } = const { recipientName, date, time, timezone, location, agentName, calendarUrl, cancelUrl } =
props props
return ( return (
<BaseEmailShell <BaseEmailShell
@ -480,9 +482,13 @@ export function AppointmentBookedEmail(props: AppointmentBookedEmailProps) {
Add to calendar Add to calendar
</a> </a>
</div> </div>
<div className="button-row button-row--secondary">
<a href={cancelUrl} className="secondary-button">
Cancel appointment
</a>
</div>
<p className="body-text-muted"> <p className="body-text-muted">
Please arrive a few minutes early. You can reschedule or cancel from Please arrive a few minutes early. You can reschedule from your dashboard.
your dashboard.
</p> </p>
</BaseEmailShell> </BaseEmailShell>
) )