Skip to content

Commit d6265e8

Browse files
authored
Wired up welcome email design customization modal (#26843)
## Summary Integrates all foundation and visual components into the welcome email customization modal: - **`welcome-email-customize-modal.tsx`** — Full modal with `GeneralTab` (sender name, reply-to, header image, footer), `DesignTab` (all design fields via context), `Sidebar` (tab navigation), and live `EmailPreview` - Reads current design settings from Ghost settings API on mount - Hydrates general settings when global data loads asynchronously > **Stack:** PR 3 of 3 — targets `cmraible/welcome-email-visual-components` (#26842) > > **Full stack:** #26841 (foundation) → #26842 (visual components) → this PR (wiring) ## Known TODOs - **Header image upload** — The UI shows a preview/remove control when a header image is set, but there is no upload control to add one yet. This will be wired up in a follow-up. - **Save handler** — The Save button currently closes the modal without persisting changes. Backend persistence will be added once the design columns exist in the database.
1 parent 832456d commit d6265e8

1 file changed

Lines changed: 243 additions & 13 deletions

File tree

apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx

Lines changed: 243 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,252 @@
1+
import EmailDesignModal from '../../email-design/email-design-modal';
2+
import EmailPreview from '../../email-design/email-preview';
13
import NiceModal, {useModal} from '@ebay/nice-modal-react';
2-
import {Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle} from '@tryghost/shade';
4+
import {
5+
BackgroundColorField,
6+
BodyFontField,
7+
ButtonColorField,
8+
ButtonCornersField,
9+
ButtonStyleField,
10+
DividerColorField,
11+
HeaderBackgroundField,
12+
HeadingFontField,
13+
HeadingWeightField,
14+
ImageCornersField,
15+
LinkColorField,
16+
LinkStyleField,
17+
SectionTitleColorField
18+
} from '../../email-design/design-fields';
19+
import {DEFAULT_EMAIL_DESIGN, type EmailDesignSettings} from '../../email-design/types';
20+
import {EmailDesignProvider} from '../../email-design/email-design-context';
21+
import {
22+
Input,
23+
Separator,
24+
Switch,
25+
Tabs,
26+
TabsContent,
27+
TabsList,
28+
TabsTrigger,
29+
Textarea
30+
} from '@tryghost/shade';
31+
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
32+
import {useCallback, useState} from 'react';
33+
import {useGlobalData} from '../../../providers/global-data-provider';
34+
35+
interface GeneralSettings {
36+
senderName: string;
37+
replyToEmail: string;
38+
headerImage: string;
39+
showPublicationTitle: boolean;
40+
emailFooter: string;
41+
}
42+
43+
interface GeneralTabProps {
44+
generalSettings: GeneralSettings;
45+
onGeneralChange: (updates: Partial<GeneralSettings>) => void;
46+
siteTitle: string | undefined;
47+
emailDomain: string;
48+
}
49+
50+
const GeneralTab: React.FC<GeneralTabProps> = ({generalSettings, onGeneralChange, siteTitle, emailDomain}) => (
51+
<div className="flex flex-col gap-6 pt-6">
52+
<section>
53+
<h4 className="text-gray-500 mb-4 text-xs font-semibold uppercase tracking-wide">Email info</h4>
54+
<div className="flex flex-col gap-4">
55+
<div className="flex flex-col gap-1.5">
56+
<label className="text-sm">Sender name</label>
57+
<Input
58+
placeholder={siteTitle || 'Your site name'}
59+
value={generalSettings.senderName}
60+
onChange={e => onGeneralChange({senderName: e.target.value})}
61+
/>
62+
</div>
63+
<div className="flex flex-col gap-1.5">
64+
<label className="text-sm">Reply-to email</label>
65+
<Input
66+
placeholder={`noreply@${emailDomain}`}
67+
value={generalSettings.replyToEmail}
68+
onChange={e => onGeneralChange({replyToEmail: e.target.value})}
69+
/>
70+
</div>
71+
</div>
72+
</section>
73+
74+
<Separator />
75+
76+
<section>
77+
<h4 className="text-gray-500 mb-4 text-xs font-semibold uppercase tracking-wide">Content</h4>
78+
<div className="flex flex-col gap-4">
79+
<div className="flex flex-col gap-1.5">
80+
<label className="text-sm">Header image</label>
81+
{generalSettings.headerImage ? (
82+
<div className="border-gray-200 dark:border-gray-800 relative overflow-hidden rounded-md border">
83+
<img
84+
alt="Header"
85+
className="h-auto w-full"
86+
src={generalSettings.headerImage}
87+
/>
88+
<button
89+
className="absolute right-2 top-2 rounded bg-black/50 px-2 py-1 text-xs text-white hover:bg-black/70"
90+
type="button"
91+
onClick={() => onGeneralChange({headerImage: ''})}
92+
>
93+
Remove
94+
</button>
95+
</div>
96+
) : (
97+
<div className="border-gray-300 text-gray-400 dark:border-gray-700 flex h-24 items-center justify-center rounded-md border border-dashed text-sm">
98+
630x140 recommended. Use a transparent PNG for best results on any background.
99+
</div>
100+
)}
101+
</div>
102+
<div className="flex items-center justify-between">
103+
<span className="text-sm">Publication title</span>
104+
<Switch
105+
checked={generalSettings.showPublicationTitle}
106+
onCheckedChange={checked => onGeneralChange({showPublicationTitle: checked})}
107+
/>
108+
</div>
109+
<div className="flex flex-col gap-1.5">
110+
<label className="text-sm">Email footer</label>
111+
<Textarea
112+
placeholder="Any extra information or legal text"
113+
rows={3}
114+
value={generalSettings.emailFooter}
115+
onChange={e => onGeneralChange({emailFooter: e.target.value})}
116+
/>
117+
</div>
118+
</div>
119+
</section>
120+
</div>
121+
);
122+
123+
const DesignTab: React.FC = () => (
124+
<div className="flex flex-col gap-6 pt-6">
125+
<section>
126+
<h4 className="text-gray-500 mb-4 text-xs font-semibold uppercase tracking-wide">Global</h4>
127+
<div className="flex flex-col gap-4">
128+
<BackgroundColorField />
129+
<HeadingFontField />
130+
<HeadingWeightField />
131+
<BodyFontField />
132+
</div>
133+
</section>
134+
135+
<Separator />
136+
137+
<section>
138+
<h4 className="text-gray-500 mb-4 text-xs font-semibold uppercase tracking-wide">Header</h4>
139+
<div className="flex flex-col gap-4">
140+
<HeaderBackgroundField />
141+
</div>
142+
</section>
143+
144+
<Separator />
145+
146+
<section>
147+
<h4 className="text-gray-500 mb-4 text-xs font-semibold uppercase tracking-wide">Body</h4>
148+
<div className="flex flex-col gap-4">
149+
<SectionTitleColorField />
150+
<ButtonColorField />
151+
<ButtonStyleField />
152+
<ButtonCornersField />
153+
<LinkColorField />
154+
<LinkStyleField />
155+
<ImageCornersField />
156+
<DividerColorField />
157+
</div>
158+
</section>
159+
</div>
160+
);
161+
162+
interface SidebarProps {
163+
generalSettings: GeneralSettings;
164+
onGeneralChange: (updates: Partial<GeneralSettings>) => void;
165+
siteTitle: string | undefined;
166+
emailDomain: string;
167+
}
168+
169+
const Sidebar: React.FC<SidebarProps> = ({generalSettings, onGeneralChange, siteTitle, emailDomain}) => (
170+
<Tabs defaultValue="general" variant="underline">
171+
<TabsList>
172+
<TabsTrigger value="general">General</TabsTrigger>
173+
<TabsTrigger value="design">Design</TabsTrigger>
174+
</TabsList>
175+
<TabsContent value="general">
176+
<GeneralTab
177+
emailDomain={emailDomain}
178+
generalSettings={generalSettings}
179+
siteTitle={siteTitle}
180+
onGeneralChange={onGeneralChange}
181+
/>
182+
</TabsContent>
183+
<TabsContent value="design">
184+
<DesignTab />
185+
</TabsContent>
186+
</Tabs>
187+
);
3188

4189
const WelcomeEmailCustomizeModal = NiceModal.create(() => {
5190
const modal = useModal();
191+
const {siteData, settings: globalSettings, config} = useGlobalData();
192+
const [siteTitle, defaultEmailAddress, supportEmailAddress] = getSettingValues<string>(globalSettings, ['title', 'default_email_address', 'support_email_address']);
193+
194+
const [designSettings, setDesignSettings] = useState<EmailDesignSettings>({...DEFAULT_EMAIL_DESIGN});
195+
const [generalSettings, setGeneralSettings] = useState<GeneralSettings>({
196+
senderName: siteTitle || '',
197+
replyToEmail: supportEmailAddress || defaultEmailAddress || '',
198+
headerImage: '',
199+
showPublicationTitle: true,
200+
emailFooter: ''
201+
});
202+
203+
const handleDesignChange = useCallback((updates: Partial<EmailDesignSettings>) => {
204+
setDesignSettings(prev => ({...prev, ...updates}));
205+
}, []);
206+
207+
const handleGeneralChange = useCallback((updates: Partial<GeneralSettings>) => {
208+
setGeneralSettings(prev => ({...prev, ...updates}));
209+
}, []);
210+
211+
const handleSave = useCallback(() => {
212+
// TODO: persist to backend when design columns are added
213+
modal.remove();
214+
}, [modal]);
215+
216+
const handleClose = useCallback(() => {
217+
modal.remove();
218+
}, [modal]);
219+
220+
const emailDomain = (config?.emailDomain as string) || defaultEmailAddress?.split('@')[1] || '';
6221

7222
return (
8-
<Dialog open onOpenChange={() => modal.remove()}>
9-
<DialogContent data-testid='welcome-email-customize-modal'>
10-
<DialogHeader>
11-
<DialogTitle>Customize welcome emails</DialogTitle>
12-
</DialogHeader>
13-
<p className='text-muted-foreground text-sm'>Design customization options coming soon.</p>
14-
<DialogFooter>
15-
<Button variant='outline' onClick={() => modal.remove()}>Close</Button>
16-
<Button onClick={() => modal.remove()}>Save</Button>
17-
</DialogFooter>
18-
</DialogContent>
19-
</Dialog>
223+
<EmailDesignProvider accentColor={siteData.accent_color} settings={designSettings} onSettingsChange={handleDesignChange}>
224+
<EmailDesignModal
225+
preview={
226+
<EmailPreview
227+
emailFooter={generalSettings.emailFooter}
228+
headerImage={generalSettings.headerImage}
229+
senderEmail={defaultEmailAddress || `noreply@${emailDomain}`}
230+
senderName={generalSettings.senderName || siteTitle || 'Your site'}
231+
settings={designSettings}
232+
showPublicationTitle={generalSettings.showPublicationTitle}
233+
subject={`Welcome to ${generalSettings.senderName || siteTitle || 'our publication'}`}
234+
/>
235+
}
236+
sidebar={
237+
<Sidebar
238+
emailDomain={emailDomain}
239+
generalSettings={generalSettings}
240+
siteTitle={siteTitle}
241+
onGeneralChange={handleGeneralChange}
242+
/>
243+
}
244+
testId="welcome-email-customize-modal"
245+
title="Welcome emails"
246+
onClose={handleClose}
247+
onSave={handleSave}
248+
/>
249+
</EmailDesignProvider>
20250
);
21251
});
22252

0 commit comments

Comments
 (0)