Skip to content

Commit 6c2e0fc

Browse files
piersolhthaninbew
andauthored
create email editor (#105)
* create webpage * initial styling enhancements + routing imporvements as per pr feedback * improve styling - match the hifi * more styling updates to match figma * fix tailwind config to be more modern * refactor: update imports and fix bug in Email components * fix: remove unnecessary blank line in ApiClient class * chore: regenerate yarn lock * chore: fix CI with imports * wrong button import lol * feat: enhance EmailEditor layout, wrapped in flexbox. improved responsiveness. --------- Co-authored-by: Thanin Kongkiatsophon <108406347+thaninbew@users.noreply.github.com>
1 parent 209cf3a commit 6c2e0fc

22 files changed

Lines changed: 4255 additions & 2910 deletions

apps/frontend/src/api/apiClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export class ApiClient {
127127
this.handleAxiosError(err, 'Failed to reset password');
128128
}
129129
}
130-
130+
131131
public async getActiveGoalSummary(): Promise<ActiveGoalResponse> {
132132
try {
133133
const res = await this.axiosInstance.get('/api/donations/goal/active');

apps/frontend/src/app.tsx

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import { ResetPasswordPage } from '@containers/auth/ResetPasswordPage';
1515
import { ConfirmRegisteredPage } from '@containers/auth/ConfirmRegisteredPage';
1616
import { DashboardPage } from '@containers/dashboard/DashboardPage';
1717
import { DonorStatsChart } from '@components/DonorStatsChart';
18-
import SidebarTester from '@containers/dashboard/sidebar/SidebarTester';
19-
import EditDonationGoalTester from '@components/DonationGoal/EditDonationGoalTester';
18+
import DashboardOverview from '@containers/dashboard/sidebar/DashboardOverview';
19+
import { EmailEditor } from './components/EmailComms/EmailEditorOverviewPage';
2020
import { AdminGrowingGoalTester } from '@containers/dashboard/AdminGrowingGoalTester';
21+
import OverviewPage from '@containers/dashboard/OverviewPage';
2122

2223
const router = createBrowserRouter([
2324
{
@@ -37,6 +38,25 @@ const router = createBrowserRouter([
3738
path: '/confirm-registered',
3839
element: <ConfirmRegisteredPage />,
3940
},
41+
{
42+
path: '/overview',
43+
// element: <ProtectedRoute />,
44+
children: [
45+
{
46+
element: <DashboardOverview />,
47+
children: [
48+
{
49+
path: '',
50+
element: <OverviewPage />,
51+
},
52+
{
53+
path: 'email',
54+
element: <EmailEditor />,
55+
},
56+
],
57+
},
58+
],
59+
},
4060
{
4161
path: '/dashboard',
4262
element: <ProtectedRoute />,
@@ -47,14 +67,6 @@ const router = createBrowserRouter([
4767
},
4868
],
4969
},
50-
{
51-
path: '/sidebar-test',
52-
element: <SidebarTester />,
53-
},
54-
{
55-
path: '/edit-donation-goal-test',
56-
element: <EditDonationGoalTester />,
57-
},
5870
{
5971
path: '/test',
6072
element: <TestimonialTester />,
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import type { TabId, EmailData, EmailsState, Signature } from './types';
2+
import EmailTextEditor from './EmailTextEditor';
3+
import { Button } from '../ui/button';
4+
import { Input } from '../ui/input';
5+
import SignatureEditorCard from './SignatureEditorCard';
6+
import { Label } from '../ui/label';
7+
8+
type EmailEditorCardProps = {
9+
activeTab: TabId;
10+
emails: EmailsState;
11+
onEmailChange: (tab: TabId, field: keyof EmailData, value: string) => void;
12+
sig: Signature;
13+
onSigChange: (sig: Signature) => void;
14+
ctaText: string;
15+
onCtaTextChange: (val: string) => void;
16+
ctaLink: string;
17+
onLinkChange: (val: string) => void;
18+
saved: boolean;
19+
sent: boolean;
20+
onSave: () => void;
21+
onSend: () => void;
22+
};
23+
24+
export default function EmailEditorCard({
25+
activeTab,
26+
emails,
27+
onEmailChange,
28+
sig,
29+
onSigChange,
30+
ctaText,
31+
onCtaTextChange: onCtaChange,
32+
ctaLink,
33+
onLinkChange: onCtaLinkChange,
34+
saved,
35+
sent,
36+
onSave,
37+
onSend,
38+
}: EmailEditorCardProps) {
39+
const currentEmail = emails[activeTab];
40+
41+
return (
42+
<div className="flex flex-col gap-5 max-w-3xl">
43+
<div className="flex flex-col gap-1.5">
44+
<Label className="pb-2"> Subject Line</Label>
45+
46+
<Input
47+
value={currentEmail.subject}
48+
onChange={(e) => onEmailChange(activeTab, 'subject', e.target.value)}
49+
className="bg-white border-slate-300 rounded-md px-4 py-2 text-sm focus:ring-2 focus:ring-emerald-600 outline-none transition text-[#171717]"
50+
placeholder="Email subject…"
51+
/>
52+
</div>
53+
54+
<div className="flex flex-col gap-1.5">
55+
<Label> Body Text</Label>
56+
57+
<EmailTextEditor
58+
key={activeTab}
59+
content={currentEmail.body}
60+
onUpdate={(html) => onEmailChange(activeTab, 'body', html)}
61+
/>
62+
63+
<Label className="pt-1 bg-slate"> Button Text </Label>
64+
65+
<Input
66+
value={ctaText}
67+
onChange={(e) => onCtaChange(e.target.value)}
68+
className="bg-white border-slate-300 rounded-md px-4 py-2 text-sm focus:ring-2 focus:ring-emerald-600 outline-none transition text-[#171717]"
69+
/>
70+
71+
<Label className="pt-2 bg-slate"> Button Link </Label>
72+
73+
<Input
74+
value={ctaLink}
75+
onChange={(e) => onCtaLinkChange(e.target.value)}
76+
className="bg-white border-slate-300 rounded-md px-4 py-2 text-sm focus:ring-2 focus:ring-emerald-600 outline-none transition text-[#171717]"
77+
/>
78+
</div>
79+
80+
<div className="flex flex-col gap-3 bg-white border border-slate-100 rounded-md p-5 shadow-sm">
81+
<Label className="pb-2"> Email Signature </Label>
82+
83+
<SignatureEditorCard sig={sig} onChange={onSigChange} />
84+
</div>
85+
86+
<div className="flex items-center gap-3 pt-1">
87+
<Button
88+
onClick={onSave}
89+
className={`px-6 py-2 h-[38px] rounded-md text-white bg-[#007B64] font-medium text-sm transition-all`}
90+
>
91+
{saved ? 'Saved' : 'Save Changes'}
92+
</Button>
93+
94+
<Button
95+
onClick={onSend}
96+
className="px-6 py-2 h-[38px] rounded-md font-medium text-sm bg-[#737373] text-white transition-all"
97+
>
98+
{sent ? 'Sent!' : 'Send Email'}
99+
</Button>
100+
</div>
101+
</div>
102+
);
103+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { useState } from 'react';
2+
import EmailEditorCard from './EmailEditorCard';
3+
import EmailPreviewPanel from './EmailPreviewPanel';
4+
import type { TabId, EmailData, EmailsState, Signature } from './types';
5+
import { Button } from '../ui/button';
6+
import {
7+
defaultEmails,
8+
DEFAULT_SIGNATURE,
9+
TAB_CONFIG,
10+
buildSignatureHTML,
11+
} from './types';
12+
13+
export function EmailEditor() {
14+
const [activeTab, setActiveTab] = useState<TabId>('donation');
15+
const [emails, setEmails] = useState<EmailsState>(defaultEmails);
16+
const [sig, setSig] = useState<Signature>(DEFAULT_SIGNATURE);
17+
const [saved, setSaved] = useState(false);
18+
const [sent, setSent] = useState(false);
19+
const [ctaText, setCtaText] = useState('DONATE AT OUR SITE!');
20+
const [ctaLink, setCtaLink] = useState('https://fenwaycommunitycenter.org/');
21+
22+
const handleEmailChange = (
23+
tab: TabId,
24+
field: keyof EmailData,
25+
value: string,
26+
) => {
27+
setEmails((prev) => ({ ...prev, [tab]: { ...prev[tab], [field]: value } }));
28+
};
29+
30+
const handleSave = () => {
31+
console.log('[EmailEditorOverviewPage] Save payload:', {
32+
tab: activeTab,
33+
subject: emails[activeTab].subject,
34+
body: emails[activeTab].body,
35+
signature: sig,
36+
});
37+
setSaved(true);
38+
setTimeout(() => setSaved(false), 2500);
39+
};
40+
41+
const handleSend = () => {
42+
const email = emails[activeTab];
43+
const fullHTML = `
44+
<html><body>
45+
${email.body}
46+
${buildSignatureHTML(sig)}
47+
${ctaText ? `<div style="text-align:center;margin:28px 0"><a href="#" style="background:#059669;color:white;padding:14px 32px;border-radius:8px;font-weight:700;text-decoration:none;font-size:14px;letter-spacing:0.05em">${ctaText}</a></div>` : ''}
48+
</body></html>
49+
`;
50+
console.log('[EmailEditorOverviewPage] Send payload:', {
51+
to: '[recipient]',
52+
subject: email.subject,
53+
html: fullHTML,
54+
});
55+
setSent(true);
56+
setTimeout(() => setSent(false), 2500);
57+
};
58+
59+
return (
60+
<div className="relative flex flex-col gap-5 p-6 bg-[#EEEEEE] min-h-screen font-sans">
61+
<div className="flex flex-row flex-1 gap-14 items-start justify-center">
62+
<div className="w-full max-w-[600px] flex-shrink-0 flex flex-col gap-5">
63+
<div className="flex items-center h-10 bg-white rounded-md border border-slate-100 w-fit">
64+
{TAB_CONFIG.map((tab) => (
65+
<Button
66+
key={tab.id}
67+
onClick={() => setActiveTab(tab.id)}
68+
className={`flex items-center h-10 px-4 py-2 rounded-md text-sm font-medium transition-all ${
69+
activeTab === tab.id
70+
? 'bg-emerald-700 text-white'
71+
: 'text-neutral-700 hover:bg-neutral-100'
72+
}`}
73+
>
74+
{tab.label}
75+
</Button>
76+
))}
77+
</div>
78+
79+
<EmailEditorCard
80+
activeTab={activeTab}
81+
emails={emails}
82+
onEmailChange={handleEmailChange}
83+
sig={sig}
84+
onSigChange={setSig}
85+
ctaText={ctaText}
86+
onCtaTextChange={setCtaText}
87+
ctaLink={ctaLink}
88+
onLinkChange={setCtaLink}
89+
saved={saved}
90+
sent={sent}
91+
onSave={handleSave}
92+
onSend={handleSend}
93+
/>
94+
</div>
95+
96+
<div className="w-full max-w-[600px] flex-shrink sticky top-6 overflow-hidden">
97+
<EmailPreviewPanel
98+
activeTab={activeTab}
99+
emails={emails}
100+
sig={sig}
101+
ctaText={ctaText}
102+
ctaLink={ctaLink}
103+
/>
104+
</div>
105+
</div>
106+
</div>
107+
);
108+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { EmailTabId, EmailsState, Signature } from './types';
2+
import { buildSignatureHTML } from './types';
3+
import FCCEmailHeader from './FCCEmailHeader.svg';
4+
import FCCEmailBg from './FCCEmailBg.png';
5+
import { Label } from '../ui/label';
6+
import { Button } from '../ui/button';
7+
8+
type EmailPreviewPanelProps = {
9+
activeTab: EmailTabId;
10+
emails: EmailsState;
11+
sig: Signature;
12+
ctaText: string;
13+
ctaLink: string;
14+
};
15+
16+
export default function EmailPreviewPanel({
17+
activeTab,
18+
emails,
19+
sig,
20+
ctaText,
21+
ctaLink,
22+
}: EmailPreviewPanelProps) {
23+
const email = emails[activeTab];
24+
25+
return (
26+
<div className="flex flex-col gap-5 h-full sticky top-6">
27+
<Label>Email Preview</Label>
28+
29+
<div className="bg-white border-1 border-[#D4D4D4] overflow-hidden rounded-xl flex flex-col h-fit">
30+
<img
31+
src={FCCEmailHeader}
32+
alt="Boston skyline"
33+
className="w-full shrink-0"
34+
/>
35+
36+
<div className="px-8 py-6 flex-1 min-h-[300px]">
37+
<div
38+
className="prose prose-sm max-w-none text-slate-700 leading-relaxed [overflow-wrap:anywhere] [word-break:break-word]"
39+
dangerouslySetInnerHTML={{ __html: email.body }}
40+
/>
41+
</div>
42+
43+
<div className="flex justify-center pb-12">
44+
<a
45+
href={ctaLink}
46+
target="_blank"
47+
rel="noreferrer"
48+
className="flex justify-center w-full"
49+
>
50+
<Button className="px-6 py-4 h-[44px] rounded-md text-white bg-[#2A9D90] font-bold text-sm transition-all flex items-center justify-center">
51+
{ctaText}
52+
</Button>
53+
</a>
54+
</div>
55+
56+
<div className="relative">
57+
<img src={FCCEmailBg} alt="FCC footer" className="w-full block" />
58+
59+
<div
60+
className="absolute inset-0 flex items-center px-8"
61+
dangerouslySetInnerHTML={{ __html: buildSignatureHTML(sig) }}
62+
/>
63+
</div>
64+
</div>
65+
</div>
66+
);
67+
}

0 commit comments

Comments
 (0)