From 7c74a5c398572f81f3afb9ab00ff04c1b947aad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Ckirukib=E2=80=9D?= <“kirubeljkl679@gmail.com”> Date: Thu, 12 Mar 2026 00:23:10 +0300 Subject: [PATCH] feat: white preview bg and html export Made-with: Cursor --- README.md | 27 ++++- public/YaltopiaHomesLogo.svg | 203 +++++++++++++++++++++++++++++++++++ src/App.css | 61 +++++++---- src/App.tsx | 73 ++++++++++--- src/index.css | 4 +- 5 files changed, 330 insertions(+), 38 deletions(-) create mode 100644 public/YaltopiaHomesLogo.svg diff --git a/README.md b/README.md index b83734a..c47068d 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,36 @@ All templates are wrapped with `BaseEmailShell`, which lives in `src/email/BaseE These templates are written as plain React components so they can be adapted to Resend in several ways: 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. +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 ``) 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. When integrating, replace the sample props in `sampleData.ts` with real data and ensure links (payment, calendar, reset, etc.) are generated by your backend. +#### Example: sending via Resend with React templates + +```ts +import { Resend } from 'resend' +import { InvitationTeamMemberEmail } from './src/email/templates' + +const resend = new Resend(process.env.RESEND_API_KEY) + +await resend.emails.send({ + from: 'Yaltopia Home ', + to: 'user@example.com', + subject: 'You have been invited to Yaltopia Home', + react: ( + + ), +}) +``` + + # React + TypeScript + Vite This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. diff --git a/public/YaltopiaHomesLogo.svg b/public/YaltopiaHomesLogo.svg new file mode 100644 index 0000000..bae08ce --- /dev/null +++ b/public/YaltopiaHomesLogo.svg @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/App.css b/src/App.css index 639c1df..5a1d0df 100644 --- a/src/App.css +++ b/src/App.css @@ -4,15 +4,14 @@ grid-template-columns: 280px minmax(0, 1fr); gap: 1px; padding: 32px 40px; - color: #f9fafb; + color: #020617; } .app-panel { border-radius: 24px; - background: rgba(15, 23, 42, 0.92); - border: 1px solid rgba(148, 163, 184, 0.25); - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.7); - backdrop-filter: blur(20px); + background: #ffffff; + border: 1px solid rgba(15, 23, 42, 0.06); + box-shadow: 0 18px 45px rgba(15, 23, 42, 0.12); } .app-sidebar { @@ -39,7 +38,7 @@ width: 28px; height: 28px; border-radius: 999px; - border: 1px solid rgba(248, 250, 252, 0.8); + border: 1px solid rgba(15, 23, 42, 0.8); display: flex; align-items: center; justify-content: center; @@ -51,7 +50,7 @@ font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; - color: #e5e7eb; + color: #020617; } .app-tag { @@ -60,8 +59,8 @@ letter-spacing: 0.16em; padding: 4px 10px; border-radius: 999px; - border: 1px solid rgba(148, 163, 184, 0.5); - color: #cbd5f5; + border: 1px solid rgba(15, 23, 42, 0.12); + color: #4b5563; } .sidebar-title { @@ -71,14 +70,14 @@ .sidebar-subtitle { font-size: 12px; - color: #9ca3af; + color: #6b7280; margin: 0; } .template-list { margin: 4px 0 0; padding: 10px 0 0; - border-top: 1px solid rgba(148, 163, 184, 0.4); + border-top: 1px solid rgba(15, 23, 42, 0.06); display: flex; flex-direction: column; gap: 4px; @@ -88,7 +87,7 @@ font-size: 11px; text-transform: uppercase; letter-spacing: 0.12em; - color: #6b7280; + color: #9ca3af; margin: 4px 0 6px; } @@ -100,13 +99,13 @@ border-radius: 999px; cursor: pointer; font-size: 12px; - color: #e5e7eb; + color: #111827; border: 1px solid transparent; } .template-item:hover { - background: rgba(15, 23, 42, 0.85); - border-color: rgba(148, 163, 184, 0.55); + background: #f3f4f6; + border-color: rgba(15, 23, 42, 0.08); } .template-item--active { @@ -119,7 +118,7 @@ font-size: 10px; padding: 2px 6px; border-radius: 999px; - border: 1px solid rgba(148, 163, 184, 0.6); + border: 1px solid rgba(148, 163, 184, 0.7); } .sidebar-footer { @@ -130,7 +129,7 @@ flex-direction: column; gap: 6px; font-size: 11px; - color: #9ca3af; + color: #6b7280; } .sidebar-footer span { @@ -169,14 +168,14 @@ font-size: 11px; padding: 4px 10px; border-radius: 999px; - border: 1px solid rgba(148, 163, 184, 0.7); - background: rgba(15, 23, 42, 0.9); + border: 1px solid rgba(148, 163, 184, 0.6); + background: #f9fafb; cursor: pointer; } .chip--active { - background: #f9fafb; - color: #020617; + background: #020617; + color: #f9fafb; } .preview-frame { @@ -198,9 +197,9 @@ .preview-email-surface { border-radius: 24px; - background: radial-gradient(circle at top, #f9fafb 0, #e5e7eb 42%, #d1d5db 100%); + background: #f3f4f6; padding: 16px 10px 20px; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.55); + box-shadow: 0 20px 45px rgba(15, 23, 42, 0.18); } .preview-email-window { @@ -208,6 +207,22 @@ overflow: hidden; } +.html-output { + width: 100%; + height: 100%; + min-height: 360px; + border-radius: 16px; + border: 1px solid rgba(15, 23, 42, 0.12); + padding: 12px 14px; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, + 'Liberation Mono', 'Courier New', monospace; + font-size: 11px; + white-space: pre; + resize: vertical; + background: #f9fafb; + color: #020617; +} + @media (max-width: 960px) { .app-shell { grid-template-columns: minmax(0, 1fr); diff --git a/src/App.tsx b/src/App.tsx index 37e462d..46fe15e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,22 +1,42 @@ import './App.css' import { templates } from './email/sampleData' import type { ComponentType } from 'react' -import { useState } from 'react' +import { useMemo, useState } from 'react' import type { TemplateId } from './email/sampleData' +import { renderToStaticMarkup } from 'react-dom/server' type TemplateEntry = (typeof templates)[number] type DeviceMode = 'desktop' | 'mobile' +type ContentMode = 'preview' | 'html' function App() { const [selectedId, setSelectedId] = useState('invitation') const [deviceMode, setDeviceMode] = useState('desktop') + const [contentMode, setContentMode] = useState('preview') const selected: TemplateEntry = templates.find((t) => t.id === selectedId) ?? templates[0] const SelectedComponent = selected.component as ComponentType + const html = useMemo(() => { + const markup = renderToStaticMarkup( + // eslint-disable-next-line react/jsx-props-no-spreading + , + ) + return ` + + + + ${selected.label} · Yaltopia Home + + + ${markup} + +` + }, [SelectedComponent, selected]) + return (
-
-
-
-
- + {contentMode === 'preview' ? ( +
+
+
+
+ {/* eslint-disable-next-line react/jsx-props-no-spreading */} + +
-
+ ) : ( +
+