Skip to content

Commit 3e53d89

Browse files
committed
feat(email): storybook like email package
1 parent 442f3d2 commit 3e53d89

24 files changed

+1358
-119
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/node_modules
1+
node_modules
22
*.swp
33
*.swo
44
*.orig

package-lock.json

Lines changed: 235 additions & 118 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
"#cypress/*": "./cypress/*.ts"
88
},
99
"main": "src/index.js",
10+
"workspaces": [
11+
"packages/*"
12+
],
1013
"scripts": {
1114
"build": "run-s build:**",
1215
"build:assets": "vite build --clearScreen false",
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//
2+
3+
export class ChangeView extends HTMLElement {
4+
static tag = "x-change-view" as const;
5+
static define() {
6+
customElements.define(ChangeView.tag, this);
7+
}
8+
constructor() {
9+
super();
10+
this.attachShadow({ mode: "open" });
11+
}
12+
connectedCallback() {
13+
this.#render();
14+
15+
this.#inputs.forEach((element) => {
16+
element.addEventListener("click", () => {
17+
this.#view = element.value as "desktop" | "html";
18+
});
19+
});
20+
}
21+
#render() {
22+
if (!this.shadowRoot) return;
23+
this.shadowRoot.innerHTML = (
24+
<ChangeViewUI view={ChangeView.current} />
25+
).toString();
26+
}
27+
28+
//
29+
30+
static get current(): "desktop" | "html" {
31+
const view = new URLSearchParams(location.search).get("view");
32+
return view === "html" ? view : "desktop";
33+
}
34+
35+
set #view(value: "desktop" | "html") {
36+
const url = new URL(location.href);
37+
url.searchParams.set("view", value);
38+
window.location.href = url.toString();
39+
}
40+
41+
//
42+
43+
get #inputs() {
44+
if (!this.shadowRoot) throw new Error("ShadowRoot not found");
45+
return this.shadowRoot.querySelectorAll("button");
46+
}
47+
}
48+
49+
ChangeView.define();
50+
51+
function ChangeViewUI({ view }: { view: string }) {
52+
return (
53+
<nav>
54+
<button disabled={view === "desktop"} value="desktop">
55+
Desktop
56+
</button>
57+
<button disabled={view === "html"} value="html">
58+
HTML
59+
</button>
60+
</nav>
61+
);
62+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
//
2+
3+
type SendEmailOptions = {
4+
sender: { name: string; email: string };
5+
to: {
6+
name: string;
7+
email: string;
8+
}[];
9+
subject: string;
10+
htmlContent: string;
11+
};
12+
13+
//
14+
15+
export class SendEmailFormWebComponent extends HTMLElement {
16+
static tag = "x-send-email-form" as const;
17+
static define() {
18+
customElements.define(SendEmailFormWebComponent.tag, this);
19+
}
20+
static BREVO_API_KEY = import.meta.env["VITE_BREVO_API_KEY"];
21+
22+
//
23+
24+
constructor() {
25+
super();
26+
this.attachShadow({ mode: "open" });
27+
}
28+
29+
connectedCallback() {
30+
if (!SendEmailFormWebComponent.BREVO_API_KEY) {
31+
console.warn(
32+
"No API key found for brevo, please set VITE_BREVO_API_KEY env variable if you want to test the email in a real email client environment",
33+
);
34+
return;
35+
}
36+
37+
this.#render();
38+
39+
this.#form.addEventListener("submit", (e) => {
40+
e.preventDefault();
41+
this.submit(this.innerHTML);
42+
});
43+
}
44+
45+
async submit(template: string) {
46+
const headers = new Headers();
47+
headers.append("accept", "application/json");
48+
headers.append("api-key", SendEmailFormWebComponent.BREVO_API_KEY);
49+
headers.append("content-type", "application/json");
50+
51+
await fetch("https://api.brevo.com/v3/smtp/email", {
52+
method: "POST",
53+
headers,
54+
body: JSON.stringify({
55+
htmlContent: template,
56+
sender: {
57+
name: "MonComptePro",
58+
email: "nepasrepondre@email.moncomptepro.beta.gouv.fr",
59+
},
60+
subject: this.#object.value,
61+
to: [{ name: "Ike Proconnect", email: this.#to.value }],
62+
} as SendEmailOptions),
63+
redirect: "follow",
64+
});
65+
}
66+
67+
//
68+
69+
#render() {
70+
if (!this.shadowRoot) return;
71+
this.shadowRoot.innerHTML = (
72+
<details
73+
style={{
74+
backgroundColor: "white",
75+
boxShadow: "0px 2px 6px 0px rgba(0, 0, 18, 0.16)",
76+
position: "fixed",
77+
bottom: "8px",
78+
right: "8px",
79+
}}
80+
>
81+
<summary style={{ background: "gainsboro", padding: "8px" }}>
82+
📨
83+
</summary>
84+
85+
<form
86+
style={{
87+
display: "flex",
88+
flexDirection: "column",
89+
padding: "8px",
90+
}}
91+
>
92+
<label style={{ display: "flex", alignItems: "center" }}>
93+
<div style={{ marginRight: "8px" }}>To</div>
94+
<input
95+
name="to"
96+
value="ike.proconnect@yopmail.com"
97+
style={{ flexGrow: 1 }}
98+
/>
99+
</label>
100+
<label style={{ display: "flex", alignItems: "center" }}>
101+
<div style={{ marginRight: "8px" }}>Subject</div>
102+
<input
103+
name="object"
104+
value="[Localhost] test email"
105+
style={{ flexGrow: 1 }}
106+
/>
107+
</label>
108+
<div
109+
style={{
110+
display: "flex",
111+
alignItems: "end",
112+
justifyContent: "space-between",
113+
}}
114+
>
115+
<p style={{ marginBottom: "0" }}>
116+
Powered by <a href="https://brevo.com">Brevo</a>
117+
</p>
118+
<button>Send</button>
119+
</div>
120+
</form>
121+
</details>
122+
).toString();
123+
}
124+
125+
get #form() {
126+
const element = this.shadowRoot?.querySelector("form");
127+
if (!element) throw new Error("No form found");
128+
return element;
129+
}
130+
get #to() {
131+
const element = this.shadowRoot?.querySelector("input[name=to]");
132+
if (!element) throw new Error("No input[name=to] found");
133+
return element as HTMLInputElement;
134+
}
135+
get #object() {
136+
const element = this.shadowRoot?.querySelector("input[name=object]");
137+
if (!element) throw new Error("No input[name=object] found");
138+
return element as HTMLInputElement;
139+
}
140+
}
141+
142+
SendEmailFormWebComponent.define();
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<!doctype html>
2+
<html
3+
lang="en"
4+
style="
5+
margin: 0;
6+
padding: 0;
7+
font-family:
8+
-apple-system,
9+
BlinkMacSystemFont,
10+
Segoe UI,
11+
Roboto,
12+
Oxygen-Sans,
13+
Ubuntu,
14+
Cantarell,
15+
Helvetica Neue,
16+
sans-serif;
17+
"
18+
>
19+
<head>
20+
<meta charset="UTF-8" />
21+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
22+
<title>Storybook like . Email Preview</title>
23+
</head>
24+
<body style="margin: 0; padding: 0">
25+
<div id="root"></div>
26+
<script type="module" src="/.storybook/index.tsx"></script>
27+
</body>
28+
</html>

0 commit comments

Comments
 (0)