Yaltopia-Hotel-Emails/src/App.tsx
“kirukib” a989624fe2 footer icons and social links
Made-with: Cursor
2026-04-02 11:18:39 +03:00

161 lines
5.6 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react'
import { brandDefaults } from './brand/brandDefaults'
import type { Brand } from './email/types'
import { interpolate } from './email/interpolate'
import { renderEmailTemplate } from './email/render'
import { BrandEditor } from './components/BrandEditor'
import { TemplatePicker } from './components/TemplatePicker'
import { VariablesForm } from './components/VariablesForm'
import { EmailPreview } from './components/EmailPreview'
import { ExportPanel } from './components/ExportPanel'
import { templateRegistry } from './templates/templates'
function initDataForTemplate(templateVars: { key: string; defaultValue?: string }[]) {
// Keep defaults stable when brand changes; this function only depends on template.
const next: Record<string, string> = {}
for (const v of templateVars) next[v.key] = v.defaultValue ?? ''
return next
}
export default function App() {
const templates = templateRegistry
const [brand, setBrand] = useState<Brand>(brandDefaults)
const [templateId, setTemplateId] = useState<string>(templates[0]?.id ?? '')
const template = useMemo(() => templates.find((t) => t.id === templateId) ?? templates[0], [templateId, templates])
const [data, setData] = useState<Record<string, string>>(() => {
return initDataForTemplate(template?.variables ?? [])
})
const handleTemplateChange = (nextId: string) => {
setTemplateId(nextId)
// Update variables immediately so the preview/exports switch reliably.
const nextTemplate = templates.find((t) => t.id === nextId)
if (nextTemplate) setData(initDataForTemplate(nextTemplate.variables))
}
const subject = useMemo(() => {
if (!template) return ''
return interpolate(template.subjectTemplate, { ...data, hotelName: brand.hotelName })
}, [template, data, brand.hotelName])
const [html, setHtml] = useState<string>('')
const [text, setText] = useState<string>('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!template) return
let cancelled = false
// Clear previous output immediately so the iframe can't appear "stuck".
setHtml('')
setText('')
const handle = setTimeout(async () => {
setLoading(true)
setError(null)
try {
const res = await renderEmailTemplate({ brand, template, data })
if (cancelled) return
setHtml(res.html)
setText(res.text)
} catch (e) {
if (cancelled) return
setError(e instanceof Error ? e.message : 'Failed to render template')
setHtml('')
setText('')
} finally {
if (cancelled) return
setLoading(false)
}
}, 350)
return () => {
cancelled = true
clearTimeout(handle)
}
}, [brand, template, data])
const styles = {
page: {
minHeight: '100vh',
padding: 18,
background: '#0b1220',
color: '#e5e7eb',
fontFamily:
'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"',
},
grid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(360px, 1fr))',
gap: 16,
alignItems: 'start',
},
card: {
background: '#0f172a',
border: '1px solid rgba(148, 163, 184, 0.25)',
borderRadius: 14,
padding: 14,
},
sectionTitle: {
margin: 0,
fontSize: 14,
fontWeight: 700,
color: '#f1f5f9',
},
}
return (
<div style={styles.page}>
<div style={{ maxWidth: 1280, margin: '0 auto', display: 'grid', gap: 16 }}>
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
<div>
<div style={{ fontSize: 18, fontWeight: 800 }}>Resend-ready Email Templates</div>
<div style={{ fontSize: 12, opacity: 0.75, marginTop: 4 }}>
Edit branding + variables, preview the branded email, then export HTML + plain text.
</div>
</div>
<div style={{ fontSize: 12, opacity: 0.75 }}>
Template: <strong style={{ color: '#fff' }}>{template?.name ?? '-'}</strong>
</div>
</header>
<div style={styles.grid}>
<aside style={styles.card}>
<div style={{ display: 'grid', gap: 14 }}>
<BrandEditor brand={brand} onChange={setBrand} />
<TemplatePicker templates={templates} value={templateId} onChange={handleTemplateChange} />
{template ? <VariablesForm key={templateId} template={template} data={data} onChange={setData} /> : null}
<div style={{ display: 'grid', gap: 8 }}>
<div style={{ ...styles.sectionTitle, fontSize: 14 }}>Status</div>
{loading ? (
<div style={{ fontSize: 13, opacity: 0.9 }}>Rendering email...</div>
) : error ? (
<div style={{ fontSize: 13, color: '#fca5a5', whiteSpace: 'pre-wrap' }}>{error}</div>
) : (
<div style={{ fontSize: 13, opacity: 0.85 }}>Preview generated.</div>
)}
</div>
</div>
</aside>
<main style={styles.card}>
{template ? (
<div style={{ display: 'grid', gap: 16 }}>
<EmailPreview key={templateId} html={html} loading={loading} />
<ExportPanel key={templateId} subject={subject} html={html} text={text} loading={loading} />
</div>
) : (
<div>No template selected.</div>
)}
</main>
</div>
</div>
</div>
)
}