fix: Email template improvements

This commit is contained in:
Eric Gullickson
2025-12-28 16:56:36 -06:00
parent e65669fede
commit 57d2c43da7
13 changed files with 325 additions and 64 deletions

View File

@@ -30,6 +30,7 @@ import {
CascadeDeleteResult,
EmailTemplate,
UpdateEmailTemplateRequest,
PreviewTemplateResponse,
// User management types
ManagedUser,
ListUsersResponse,
@@ -278,15 +279,15 @@ export const adminApi = {
const response = await apiClient.put<EmailTemplate>(`/admin/email-templates/${key}`, data);
return response.data;
},
preview: async (key: string, variables: Record<string, string>): Promise<{ subject: string; body: string }> => {
const response = await apiClient.post<{ subject: string; body: string }>(
preview: async (key: string, variables: Record<string, string>): Promise<PreviewTemplateResponse> => {
const response = await apiClient.post<PreviewTemplateResponse>(
`/admin/email-templates/${key}/preview`,
{ variables }
);
return response.data;
},
sendTest: async (key: string): Promise<{ message?: string; error?: string; subject: string; body: string }> => {
const response = await apiClient.post<{ message?: string; error?: string; subject: string; body: string }>(
sendTest: async (key: string): Promise<{ message?: string; error?: string }> => {
const response = await apiClient.post<{ message?: string; error?: string }>(
`/admin/email-templates/${key}/test`
);
return response.data;

View File

@@ -38,6 +38,8 @@ export const AdminEmailTemplatesMobileScreen: React.FC = () => {
const [editIsActive, setEditIsActive] = useState(true);
const [previewSubject, setPreviewSubject] = useState('');
const [previewBody, setPreviewBody] = useState('');
const [previewHtml, setPreviewHtml] = useState('');
const [showHtmlPreview, setShowHtmlPreview] = useState(false);
// Queries
const { data: templates, isLoading } = useQuery({
@@ -66,6 +68,7 @@ export const AdminEmailTemplatesMobileScreen: React.FC = () => {
onSuccess: (data) => {
setPreviewSubject(data.subject);
setPreviewBody(data.body);
setPreviewHtml(data.html || '');
},
onError: () => {
toast.error('Failed to generate preview');
@@ -117,6 +120,8 @@ export const AdminEmailTemplatesMobileScreen: React.FC = () => {
setPreviewTemplate(null);
setPreviewSubject('');
setPreviewBody('');
setPreviewHtml('');
setShowHtmlPreview(false);
}, []);
const handleSave = useCallback(() => {
@@ -304,6 +309,23 @@ export const AdminEmailTemplatesMobileScreen: React.FC = () => {
</div>
) : (
<>
{/* Toggle HTML/Text Preview */}
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-slate-700">Show HTML Preview</span>
<button
onClick={() => setShowHtmlPreview(!showHtmlPreview)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors min-h-[44px] min-w-[44px] ${
showHtmlPreview ? 'bg-blue-600' : 'bg-gray-200'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
showHtmlPreview ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Subject
@@ -313,14 +335,29 @@ export const AdminEmailTemplatesMobileScreen: React.FC = () => {
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Body
</label>
<div className="px-3 py-2 bg-gray-50 border border-gray-300 rounded-lg font-mono text-sm whitespace-pre-wrap">
{previewBody}
{showHtmlPreview ? (
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
HTML Preview
</label>
<div className="border border-gray-300 rounded-lg overflow-hidden bg-gray-50">
<iframe
srcDoc={previewHtml}
style={{ width: '100%', height: '400px', border: 'none' }}
title="Email HTML Preview"
/>
</div>
</div>
</div>
) : (
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Body (Plain Text)
</label>
<div className="px-3 py-2 bg-gray-50 border border-gray-300 rounded-lg font-mono text-sm whitespace-pre-wrap">
{previewBody}
</div>
</div>
)}
</>
)}

View File

@@ -210,6 +210,12 @@ export interface UpdateEmailTemplateRequest {
isActive?: boolean;
}
export interface PreviewTemplateResponse {
subject: string;
body: string;
html: string;
}
// ============================================
// User Management types (subscription tiers)
// ============================================

View File

@@ -59,6 +59,8 @@ export const AdminEmailTemplatesPage: React.FC = () => {
const [editIsActive, setEditIsActive] = useState(true);
const [previewSubject, setPreviewSubject] = useState('');
const [previewBody, setPreviewBody] = useState('');
const [previewHtml, setPreviewHtml] = useState('');
const [showHtmlPreview, setShowHtmlPreview] = useState(false);
// Queries
const { data: templates, isLoading } = useQuery({
@@ -87,6 +89,7 @@ export const AdminEmailTemplatesPage: React.FC = () => {
onSuccess: (data) => {
setPreviewSubject(data.subject);
setPreviewBody(data.body);
setPreviewHtml(data.html || '');
setPreviewDialogOpen(true);
},
onError: () => {
@@ -141,6 +144,8 @@ export const AdminEmailTemplatesPage: React.FC = () => {
setPreviewDialogOpen(false);
setPreviewSubject('');
setPreviewBody('');
setPreviewHtml('');
setShowHtmlPreview(false);
}, []);
const handleSave = useCallback(() => {
@@ -362,6 +367,16 @@ export const AdminEmailTemplatesPage: React.FC = () => {
This preview uses sample data to show how the template will appear.
</Alert>
<FormControlLabel
control={
<Switch
checked={showHtmlPreview}
onChange={(e) => setShowHtmlPreview(e.target.checked)}
/>
}
label="Show HTML Preview"
/>
<TextField
label="Subject"
fullWidth
@@ -371,19 +386,41 @@ export const AdminEmailTemplatesPage: React.FC = () => {
}}
/>
<TextField
label="Body"
fullWidth
multiline
rows={12}
value={previewBody}
InputProps={{
readOnly: true,
}}
inputProps={{
style: { fontFamily: 'monospace' },
}}
/>
{showHtmlPreview ? (
<Box>
<Typography variant="subtitle2" gutterBottom>
HTML Preview
</Typography>
<Box
sx={{
border: '1px solid #e2e8f0',
borderRadius: 1,
overflow: 'hidden',
backgroundColor: '#f8f9fa',
}}
>
<iframe
srcDoc={previewHtml}
style={{ width: '100%', height: '500px', border: 'none' }}
title="Email HTML Preview"
/>
</Box>
</Box>
) : (
<TextField
label="Body (Plain Text)"
fullWidth
multiline
rows={12}
value={previewBody}
InputProps={{
readOnly: true,
}}
inputProps={{
style: { fontFamily: 'monospace' },
}}
/>
)}
</Box>
</DialogContent>
<DialogActions>