Skip to content

Commit ebcbb88

Browse files
committed
migration for wysiwyg email editor
1 parent 5c0ba55 commit ebcbb88

10 files changed

Lines changed: 422 additions & 81 deletions

File tree

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import mongoose from "mongoose";
2+
import { nanoid } from "nanoid";
3+
4+
function generateUniqueId() {
5+
return nanoid();
6+
}
7+
8+
mongoose.connect(process.env.DB_CONNECTION_STRING, {
9+
useNewUrlParser: true,
10+
useUnifiedTopology: true,
11+
});
12+
13+
const EmailContentBlockSchema = new mongoose.Schema({
14+
blockType: { type: String, required: true },
15+
settings: { type: Object, required: true, default: () => ({}) },
16+
});
17+
18+
const EmailStyleSchema = new mongoose.Schema({
19+
colors: { type: Object, required: true },
20+
typography: { type: Object, required: true },
21+
structure: { type: Object, required: true },
22+
});
23+
24+
const EmailMetaSchema = new mongoose.Schema({
25+
previewText: { type: String },
26+
utm: { type: Object },
27+
});
28+
29+
const EmailActionSchema = new mongoose.Schema({
30+
type: { type: String },
31+
data: { type: mongoose.Schema.Types.Mixed },
32+
});
33+
34+
export const EmailContentSchema = new mongoose.Schema({
35+
content: { type: [EmailContentBlockSchema], required: true },
36+
style: { type: EmailStyleSchema, required: true },
37+
meta: { type: EmailMetaSchema, required: true },
38+
});
39+
40+
export const EmailSchema = new mongoose.Schema({
41+
emailId: { type: String, required: true, default: generateUniqueId },
42+
content: { type: EmailContentSchema, required: true },
43+
subject: { type: String, required: true },
44+
delayInMillis: { type: Number, required: true, default: 86400000 },
45+
published: { type: Boolean, required: true, default: false },
46+
action: EmailActionSchema,
47+
});
48+
49+
export const SequenceSchema = new mongoose.Schema(
50+
{
51+
domain: { type: mongoose.Schema.Types.ObjectId, required: true },
52+
sequenceId: {
53+
type: String,
54+
required: true,
55+
},
56+
type: { type: String, required: true },
57+
emails: { type: Object, required: true },
58+
},
59+
{
60+
timestamps: true,
61+
},
62+
);
63+
64+
const Sequence = mongoose.model("Sequence", SequenceSchema);
65+
66+
const CourseSchema = new mongoose.Schema(
67+
{
68+
domain: { type: mongoose.Schema.Types.ObjectId, required: true },
69+
courseId: { type: String, required: true, default: generateUniqueId },
70+
title: { type: String, required: true },
71+
groups: [
72+
{
73+
name: { type: String, required: true },
74+
_id: {
75+
type: String,
76+
required: true,
77+
},
78+
rank: { type: Number, required: true },
79+
drip: new mongoose.Schema({
80+
type: {
81+
type: String,
82+
required: true,
83+
},
84+
status: { type: Boolean, required: true, default: false },
85+
delayInMillis: { type: Number },
86+
dateInUTC: { type: Number },
87+
email: { type: Object },
88+
}),
89+
},
90+
],
91+
},
92+
{
93+
timestamps: true,
94+
},
95+
);
96+
97+
const Course = mongoose.model("Course", CourseSchema);
98+
99+
const defaultEmailStyle = {
100+
colors: {
101+
background: "#ffffff",
102+
foreground: "#000000",
103+
border: "#e2e8f0",
104+
accent: "#0284c7",
105+
accentForeground: "#ffffff",
106+
},
107+
typography: {
108+
header: {
109+
fontFamily: "Arial, sans-serif",
110+
letterSpacing: "0px",
111+
textTransform: "none",
112+
textDecoration: "none",
113+
},
114+
text: {
115+
fontFamily: "Arial, sans-serif",
116+
letterSpacing: "0px",
117+
textTransform: "none",
118+
textDecoration: "none",
119+
},
120+
link: {
121+
fontFamily: "Arial, sans-serif",
122+
textDecoration: "underline",
123+
letterSpacing: "0px",
124+
textTransform: "none",
125+
},
126+
},
127+
interactives: {
128+
button: {
129+
padding: {
130+
x: "16px",
131+
y: "8px",
132+
},
133+
border: {
134+
width: "0px",
135+
radius: "4px",
136+
style: "solid",
137+
},
138+
},
139+
link: {
140+
padding: {
141+
x: "0px",
142+
y: "0px",
143+
},
144+
},
145+
},
146+
structure: {
147+
page: {
148+
background: "#ffffff",
149+
foreground: "#000000",
150+
width: "600px",
151+
marginY: "20px",
152+
borderWidth: "1px",
153+
borderStyle: "solid",
154+
borderRadius: "10px",
155+
},
156+
section: {
157+
padding: {
158+
x: "24px",
159+
y: "16px",
160+
},
161+
},
162+
},
163+
};
164+
165+
const migrateSequenceEmail = async (sequence) => {
166+
console.log(
167+
`Migrating sequence: ${sequence.sequenceId} (${sequence.type})`,
168+
);
169+
for (const email of sequence.emails) {
170+
if (email.content?.style) {
171+
continue;
172+
}
173+
console.log(`Migrating email: ${email.emailId} (${email.subject})`);
174+
email.content = {
175+
style: defaultEmailStyle,
176+
meta: {
177+
previewText: email.previewText || "",
178+
},
179+
content: [
180+
{
181+
blockType: "text",
182+
settings: {
183+
content: email.content,
184+
},
185+
},
186+
],
187+
};
188+
sequence.markModified("emails");
189+
await sequence.save();
190+
}
191+
};
192+
193+
const migrateSequenceEmails = async () => {
194+
const sequences = await Sequence.find({});
195+
for (const sequence of sequences) {
196+
try {
197+
await migrateSequenceEmail(sequence);
198+
} catch (error) {
199+
console.error(`Error updating homepage for domain: ${page.domain}`);
200+
console.error(error);
201+
}
202+
}
203+
};
204+
205+
const migrateDripCourses = async () => {
206+
const courses = await Course.find({});
207+
for (const course of courses) {
208+
let courseModified = false;
209+
for (const group of course.groups) {
210+
if (group.drip?.email && !group.drip.email?.content?.style) {
211+
console.log(
212+
`Migrating drip on course: ${course.title} (${group.name})`,
213+
);
214+
group.drip.email.content = {
215+
style: defaultEmailStyle,
216+
meta: {
217+
previewText: group.drip.email.previewText || "",
218+
},
219+
content: [
220+
{
221+
blockType: "text",
222+
settings: {
223+
content: group.drip.email.content,
224+
},
225+
},
226+
],
227+
};
228+
courseModified = true;
229+
}
230+
}
231+
if (courseModified) {
232+
course.markModified("groups");
233+
await course.save();
234+
}
235+
}
236+
};
237+
238+
(async () => {
239+
await migrateSequenceEmails();
240+
await migrateDripCourses();
241+
mongoose.connection.close();
242+
})();

apps/web/app/(with-contexts)/dashboard/(sidebar)/mails/broadcast/[id]/page.tsx

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -571,24 +571,20 @@ export default function Page({
571571
}
572572
onClick={onSubmit}
573573
>
574-
<div className="p-4">
575-
<p>
576-
Are you sure you want to send
577-
this email to{" "}
578-
{filteredUsersCount} contacts?
579-
</p>
580-
</div>
574+
<p>
575+
Are you sure you want to send this
576+
email to {filteredUsersCount}{" "}
577+
contacts?
578+
</p>
581579
</Dialog2>
582580
<Button
583581
variant={
584582
showScheduleInput
585-
? "classic"
586-
: "soft"
583+
? "secondary"
584+
: "ghost"
587585
}
588586
className="gap-2"
589-
onClick={(
590-
e: ChangeEvent<HTMLInputElement>,
591-
) => {
587+
onClick={() => {
592588
setShowScheduleInput(true);
593589
}}
594590
>
@@ -622,10 +618,8 @@ export default function Page({
622618
</div>
623619
</Dialog2>
624620
<Button
625-
variant="soft"
626-
onClick={(
627-
e: ChangeEvent<HTMLInputElement>,
628-
) => {
621+
variant="secondary"
622+
onClick={(e) => {
629623
e.preventDefault();
630624
setShowScheduleInput(false);
631625
setDelay(0);
@@ -646,7 +640,7 @@ export default function Page({
646640
<Clock /> Scheduled for{" "}
647641
{new Date(delay).toLocaleString()}
648642
</p>
649-
<Button variant="soft" onClick={cancelSending}>
643+
<Button variant="secondary" onClick={cancelSending}>
650644
{BUTTON_CANCEL_SCHEDULED_MAIL}
651645
</Button>
652646
</div>

apps/web/app/(with-contexts)/dashboard/(sidebar)/mails/mail-hub.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default function MailHub() {
2020

2121
const breadcrumbs = [{ label: tab, href: "#" }];
2222

23-
if (!checkPermission(profile.permissions!, [permissions.manageSite])) {
23+
if (!checkPermission(profile?.permissions!, [permissions.manageSite])) {
2424
return <LoadingScreen />;
2525
}
2626

apps/web/components/admin/mails/editor-layout.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Button2 } from "@courselit/components-library";
1+
import { Button } from "@/components/ui/button";
22
import { CheckCircled, Sync } from "@courselit/icons";
33
import { Check, Copy } from "lucide-react";
44
import { useState, useEffect, useRef } from "react";
@@ -151,7 +151,7 @@ export const VariableDisplay = ({
151151
<div className="font-mono text-sm bg-muted px-2 py-1 rounded text-slate-700 font-semibold inline-block cursor-pointer hover:bg-muted/80 transition-colors">
152152
{variable}
153153
</div>
154-
<Button2
154+
<Button
155155
variant="ghost"
156156
size="icon"
157157
className="h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity"
@@ -162,7 +162,7 @@ export const VariableDisplay = ({
162162
) : (
163163
<Copy className="h-3 w-3" />
164164
)}
165-
</Button2>
165+
</Button>
166166
</div>
167167
<p className="text-xs text-muted-foreground mt-1 ml-1">
168168
{description}

apps/web/components/admin/mails/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { FetchBuilder } from "@courselit/utils";
2323
import { useRouter } from "next/navigation";
2424
import { ThunkDispatch } from "redux-thunk";
2525
import {
26-
Button,
26+
// Button,
2727
Tabbs,
2828
Card,
2929
CardHeader,
@@ -37,6 +37,7 @@ import { AnyAction } from "redux";
3737
import RequestForm from "./request-form";
3838
import SequencesList from "./sequences-list";
3939
const { networkAction } = actionCreators;
40+
import { Button } from "@components/ui/button";
4041

4142
interface MailsProps {
4243
address: Address;

packages/components-library/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@
5252
},
5353
"gitHead": "fca7d2f8455c835f1c185a1ebc8513c6269ebe5b",
5454
"dependencies": {
55-
"@courselit/page-models": "workspace:^",
5655
"@courselit/common-models": "workspace:^",
5756
"@courselit/icons": "workspace:^",
57+
"@courselit/page-models": "workspace:^",
5858
"@courselit/text-editor": "workspace:^",
5959
"@courselit/utils": "workspace:^",
6060
"@dnd-kit/core": "^6.1.0",
@@ -63,7 +63,7 @@
6363
"@radix-ui/react-accordion": "^1.1.2",
6464
"@radix-ui/react-avatar": "^1.0.4",
6565
"@radix-ui/react-checkbox": "^1.0.4",
66-
"@radix-ui/react-dialog": "^1.0.5",
66+
"@radix-ui/react-dialog": "^1.1.14",
6767
"@radix-ui/react-dropdown-menu": "^2.0.6",
6868
"@radix-ui/react-form": "^0.0.3",
6969
"@radix-ui/react-label": "^2.1.2",
@@ -79,10 +79,10 @@
7979
"class-variance-authority": "^0.7.0",
8080
"clsx": "^2.1.0",
8181
"currency-symbol-map": "^5.1.0",
82+
"lodash.debounce": "^4.0.8",
8283
"lucide-react": "^0.309.0",
8384
"react-dom": "^18.2.0",
8485
"tailwind-merge": "^2.2.0",
85-
"tailwindcss-animate": "^1.0.7",
86-
"lodash.debounce": "^4.0.8"
86+
"tailwindcss-animate": "^1.0.7"
8787
}
8888
}

0 commit comments

Comments
 (0)