astro-html 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Banner.astro +69 -0
- package/Mail.astro +12 -0
- package/README.md +82 -0
- package/index.astro +522 -0
- package/index.js +169 -0
- package/package.json +36 -0
- package/toolbar.js +32 -0
package/Banner.astro
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
const props = Astro.props;
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<html lang="de">
|
|
6
|
+
<head>
|
|
7
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
|
8
|
+
|
|
9
|
+
<meta name="ad.orientation" content="portrait,landscape">
|
|
10
|
+
<meta name="ad.size" content={`width=${props.width},height=${props.height}`}>
|
|
11
|
+
|
|
12
|
+
<title>{props.title}</title>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
body {
|
|
16
|
+
margin: 0;
|
|
17
|
+
width: 100%;
|
|
18
|
+
height: 100%;
|
|
19
|
+
}
|
|
20
|
+
.banner {
|
|
21
|
+
display: block;
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
cursor: pointer;
|
|
24
|
+
text-decoration: none;
|
|
25
|
+
}
|
|
26
|
+
</style>
|
|
27
|
+
|
|
28
|
+
<slot name="head" />
|
|
29
|
+
</head>
|
|
30
|
+
|
|
31
|
+
<body>
|
|
32
|
+
<a href="javascript:window.open(window.clickTag)" class={['banner', props.class].join(" ")} id="clicktag" target="_blank">
|
|
33
|
+
<slot />
|
|
34
|
+
</a>
|
|
35
|
+
|
|
36
|
+
<script is:inline type="application/javascript">
|
|
37
|
+
var clickTag = "";
|
|
38
|
+
var getUriParams = (function () {
|
|
39
|
+
var query_string = {};
|
|
40
|
+
var query = window.location.search.substring(1);
|
|
41
|
+
var parmsArray = query.split("&");
|
|
42
|
+
if (parmsArray.length <= 0) return query_string;
|
|
43
|
+
for (var i = 0; i < parmsArray.length; i++) {
|
|
44
|
+
var pair = parmsArray[i].split("=");
|
|
45
|
+
var val = decodeURIComponent(pair[1]);
|
|
46
|
+
if (val != "" && pair[0] != "") query_string[pair[0]] = val;
|
|
47
|
+
}
|
|
48
|
+
return query_string;
|
|
49
|
+
})();
|
|
50
|
+
|
|
51
|
+
const element = document.getElementById("clicktag");
|
|
52
|
+
const href = getUriParams.clicktag || getUriParams.clickTag || getUriParams.clicktag1;
|
|
53
|
+
const target = getUriParams.target || getUriParams.target1;
|
|
54
|
+
|
|
55
|
+
// google
|
|
56
|
+
if (href) {
|
|
57
|
+
window.clickTag = href;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// default
|
|
61
|
+
if (element && href) {
|
|
62
|
+
element.setAttribute("href", href);
|
|
63
|
+
}
|
|
64
|
+
if (element && target) {
|
|
65
|
+
element.setAttribute("target", target);
|
|
66
|
+
}
|
|
67
|
+
</script>
|
|
68
|
+
</body>
|
|
69
|
+
</html>
|
package/Mail.astro
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Head, Html } from "@react-email/components";
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<Fragment
|
|
6
|
+
set:html={`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">`}
|
|
7
|
+
/>
|
|
8
|
+
<Html>
|
|
9
|
+
<Head />
|
|
10
|
+
|
|
11
|
+
<slot />
|
|
12
|
+
</Html>
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# astro-html
|
|
2
|
+
|
|
3
|
+
Integration for Astro to Generate HTML emails with React ([react-email](https://github.com/resend/react-email)).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm i astro-html
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
import { defineConfig } from "astro/config";
|
|
15
|
+
import email from "astro-html";
|
|
16
|
+
|
|
17
|
+
// https://astro.build/config
|
|
18
|
+
export default defineConfig({
|
|
19
|
+
integrations: [
|
|
20
|
+
email({
|
|
21
|
+
filename: "[name].html",
|
|
22
|
+
}),
|
|
23
|
+
],
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Development
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npx astro dev
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Development preview with live reload.
|
|
34
|
+
|
|
35
|
+
### Production
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npx astro build
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This will generate HTML files for each `.astro` file in the `src/pages` directory.
|
|
42
|
+
|
|
43
|
+
## Example
|
|
44
|
+
|
|
45
|
+
```astro
|
|
46
|
+
---
|
|
47
|
+
// Important! Without partial astro will render the default Doctype.
|
|
48
|
+
export const partial = true;
|
|
49
|
+
|
|
50
|
+
import * as React from "react";
|
|
51
|
+
import { Body, Container, Heading, Link, Text } from "@react-email/components";
|
|
52
|
+
import Mail from "astro-html/Mail.astro";
|
|
53
|
+
|
|
54
|
+
const fontFamily =
|
|
55
|
+
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif";
|
|
56
|
+
|
|
57
|
+
const container: React.CSSProperties = {
|
|
58
|
+
paddingLeft: "45px",
|
|
59
|
+
paddingRight: "45px",
|
|
60
|
+
margin: "0 auto",
|
|
61
|
+
maxWidth: "640px",
|
|
62
|
+
};
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
<Mail>
|
|
66
|
+
<Body
|
|
67
|
+
style={{
|
|
68
|
+
fontFamily,
|
|
69
|
+
backgroundColor: "#ffffff",
|
|
70
|
+
margin: "0",
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
<Container style={container}>
|
|
74
|
+
<Heading>My Email</Heading>
|
|
75
|
+
<Text>Lorem Ipsum</Text>
|
|
76
|
+
|
|
77
|
+
<Link href="https://example.com">Click me</Link>
|
|
78
|
+
</Container>
|
|
79
|
+
</Body>
|
|
80
|
+
</Mail>
|
|
81
|
+
|
|
82
|
+
```
|
package/index.astro
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
---
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
let failed = false;
|
|
6
|
+
|
|
7
|
+
async function findTemplates(
|
|
8
|
+
basePath: string,
|
|
9
|
+
relativePath: string = "",
|
|
10
|
+
currentDepth: number = 0,
|
|
11
|
+
maxDepth: number = 2,
|
|
12
|
+
): Promise<any[]> {
|
|
13
|
+
if (currentDepth > maxDepth) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const fullPath = path.resolve(basePath, relativePath);
|
|
18
|
+
const items = fs.readdirSync(fullPath);
|
|
19
|
+
const results: any[] = [];
|
|
20
|
+
|
|
21
|
+
for (const item of items) {
|
|
22
|
+
const itemPath = path.resolve(fullPath, item);
|
|
23
|
+
const stat = fs.statSync(itemPath);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
if (stat.isDirectory()) {
|
|
27
|
+
const subResults = await findTemplates(
|
|
28
|
+
basePath,
|
|
29
|
+
path.join(relativePath, item),
|
|
30
|
+
currentDepth + 1,
|
|
31
|
+
maxDepth,
|
|
32
|
+
);
|
|
33
|
+
results.push(...subResults);
|
|
34
|
+
} else {
|
|
35
|
+
if (!itemPath.endsWith(".astro")) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const astroPageRaw = { default: fs.readFileSync(itemPath, "utf-8") };
|
|
40
|
+
const frontMatterMatch = astroPageRaw.default?.match(
|
|
41
|
+
/^---\s*([\s\S]*?)\s*---/,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Extract only export statements from frontmatter
|
|
45
|
+
let exportStatements = "";
|
|
46
|
+
if (frontMatterMatch) {
|
|
47
|
+
const frontMatter = frontMatterMatch[1];
|
|
48
|
+
const exportMatches = frontMatter.match(
|
|
49
|
+
/export\s+[\s\S]*?(?=export\s+|$)/g,
|
|
50
|
+
);
|
|
51
|
+
if (exportMatches) {
|
|
52
|
+
exportStatements = exportMatches.join("\n");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const frontMatterToProcess = exportStatements || "";
|
|
57
|
+
|
|
58
|
+
let exprts = {};
|
|
59
|
+
|
|
60
|
+
// TODO: This limits us to not use relative file imports. Why cant we import astro file here at build time.
|
|
61
|
+
if (frontMatterMatch) {
|
|
62
|
+
const frontMatterModule = await import(
|
|
63
|
+
`data:text/javascript,${encodeURIComponent(frontMatterToProcess)}`
|
|
64
|
+
);
|
|
65
|
+
exprts = frontMatterModule;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const templateName = (
|
|
69
|
+
relativePath ? path.join(relativePath, item) : item
|
|
70
|
+
).replace(".astro", "");
|
|
71
|
+
|
|
72
|
+
// Check if getStaticPaths exists and generate multiple pages if so
|
|
73
|
+
if (
|
|
74
|
+
exprts.getStaticPaths &&
|
|
75
|
+
typeof exprts.getStaticPaths === "function"
|
|
76
|
+
) {
|
|
77
|
+
const staticPaths = await exprts.getStaticPaths();
|
|
78
|
+
|
|
79
|
+
for (const pathData of staticPaths) {
|
|
80
|
+
let pageName = templateName;
|
|
81
|
+
for (const key in pathData.params) {
|
|
82
|
+
pageName = pageName.replace(`[${key}]`, pathData.params[key]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
results.push({
|
|
86
|
+
name: pageName,
|
|
87
|
+
config: exprts.config,
|
|
88
|
+
params: pathData.params,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
results.push({
|
|
93
|
+
name: templateName,
|
|
94
|
+
config: exprts.config,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error(err);
|
|
100
|
+
failed = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return results;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const templates = await findTemplates("src/pages", "", 0, 2);
|
|
108
|
+
|
|
109
|
+
if (failed) {
|
|
110
|
+
throw new Error("Failed to build pages");
|
|
111
|
+
}
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
<html>
|
|
115
|
+
<head>
|
|
116
|
+
<title>Astro Email</title>
|
|
117
|
+
|
|
118
|
+
<Fragment
|
|
119
|
+
set:html={`
|
|
120
|
+
<script id="data" type="application/json">${JSON.stringify(
|
|
121
|
+
{
|
|
122
|
+
pages: templates,
|
|
123
|
+
},
|
|
124
|
+
null,
|
|
125
|
+
" "
|
|
126
|
+
)}</script>
|
|
127
|
+
`}
|
|
128
|
+
/>
|
|
129
|
+
</head>
|
|
130
|
+
|
|
131
|
+
<body>
|
|
132
|
+
<div class="layout">
|
|
133
|
+
<div class="sidebar">
|
|
134
|
+
<div class="sidebar-header">
|
|
135
|
+
<h1>Templates</h1>
|
|
136
|
+
<button id="download-assets" class="download-button" data-has-assets="true">
|
|
137
|
+
Download All Assets
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
{
|
|
141
|
+
templates.map((template, index) => {
|
|
142
|
+
const path = template?.name.replace(".astro", "");
|
|
143
|
+
return (
|
|
144
|
+
<div class="button" data-template={path}>
|
|
145
|
+
<a href={`#${path}`}>
|
|
146
|
+
<span class="template-number">{String(index + 1).padStart(2, '0')}</span>
|
|
147
|
+
<span class="template-name">{path}</span>
|
|
148
|
+
</a>
|
|
149
|
+
<a target="_blank" href={path} class="template-link">
|
|
150
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1em" height="1em" style="margin-bottom:-2px;" viewBox="0 0 15 15">
|
|
151
|
+
<defs>
|
|
152
|
+
<clipPath id="clip-path">
|
|
153
|
+
<rect id="Rectangle_1687" data-name="Rectangle 1687" width="15" height="15" transform="translate(11726 152)" fill="none"/>
|
|
154
|
+
</clipPath>
|
|
155
|
+
</defs>
|
|
156
|
+
<g id="Mask_Group_3" data-name="Mask Group 3" transform="translate(-11726 -152)" clip-path="url(#clip-path)">
|
|
157
|
+
<g id="Group_422" data-name="Group 422" fill="currentColor">
|
|
158
|
+
<path id="Path_4182" data-name="Path 4182" d="M14.6,14H7.4A2.4,2.4,0,0,1,5,11.6V4.4A2.4,2.4,0,0,1,7.4,2h2.8V3.2H7.4A1.2,1.2,0,0,0,6.2,4.4v7.2a1.2,1.2,0,0,0,1.2,1.2h7.2a1.2,1.2,0,0,0,1.2-1.2V9H17v2.6A2.4,2.4,0,0,1,14.6,14Z" transform="translate(11722 152)"/>
|
|
159
|
+
<path id="Path_4181" data-name="Path 4181" d="M3.385,9.591V1.927L.963,4.349a.564.564,0,0,1-.8-.8L3.54.176a.564.564,0,0,1,.684-.1h0l.01.006,0,0,.007,0,.006,0,0,0L4.264.1h0a.567.567,0,0,1,.083.069L7.733,3.55a.564.564,0,0,1-.8.8L4.513,1.926V9.591a.564.564,0,1,1-1.128,0Z" transform="translate(11737.298 150.117) rotate(45)"/>
|
|
160
|
+
</g>
|
|
161
|
+
</g>
|
|
162
|
+
</svg>
|
|
163
|
+
</a>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<div class="preview">
|
|
171
|
+
<div id="display-width" class="preview-header"></div>
|
|
172
|
+
<iframe width="640px"></iframe>
|
|
173
|
+
|
|
174
|
+
<script>
|
|
175
|
+
const dataString = document.querySelector("#data")?.innerHTML;
|
|
176
|
+
if (!dataString) throw new Error("No data found");
|
|
177
|
+
|
|
178
|
+
const data = JSON.parse(dataString);
|
|
179
|
+
console.debug("data", data);
|
|
180
|
+
|
|
181
|
+
// Download assets functionality
|
|
182
|
+
const downloadButton = document.querySelector("#download-assets");
|
|
183
|
+
const hasAssets = downloadButton && downloadButton.getAttribute("data-has-assets") === "true";
|
|
184
|
+
|
|
185
|
+
if (downloadButton) {
|
|
186
|
+
if (!hasAssets) {
|
|
187
|
+
downloadButton.disabled = true;
|
|
188
|
+
downloadButton.innerHTML = `
|
|
189
|
+
<svg width="12" height="12" viewBox="0 0 12 12" style="color: #6c757d;">
|
|
190
|
+
<circle cx="6" cy="6" r="4" stroke="currentColor" stroke-width="2" fill="none"/>
|
|
191
|
+
<path d="M4.5 4.5l3 3M7.5 4.5l-3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
192
|
+
</svg>
|
|
193
|
+
No Assets Built
|
|
194
|
+
`;
|
|
195
|
+
downloadButton.title = "Run 'npm run build' to generate downloadable assets";
|
|
196
|
+
} else {
|
|
197
|
+
downloadButton.addEventListener("click", () => {
|
|
198
|
+
// Simple direct download of pre-built zip
|
|
199
|
+
const link = document.createElement("a");
|
|
200
|
+
link.href = "/assets.zip";
|
|
201
|
+
link.download = "assets.zip";
|
|
202
|
+
document.body.appendChild(link);
|
|
203
|
+
link.click();
|
|
204
|
+
document.body.removeChild(link);
|
|
205
|
+
|
|
206
|
+
// Show success feedback
|
|
207
|
+
const originalText = downloadButton.innerHTML;
|
|
208
|
+
downloadButton.innerHTML = `
|
|
209
|
+
<svg width="12" height="12" viewBox="0 0 12 12" style="color: #22c55e;">
|
|
210
|
+
<path d="M2 6l2.5 2.5L9 4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
|
211
|
+
</svg>
|
|
212
|
+
Downloaded!
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
setTimeout(() => {
|
|
216
|
+
downloadButton.innerHTML = originalText;
|
|
217
|
+
}, 1500);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const iframe = document.querySelector("iframe");
|
|
223
|
+
if (!iframe) throw new Error("No iframe found");
|
|
224
|
+
|
|
225
|
+
let fixedWidth = undefined;
|
|
226
|
+
let fixedHeight = undefined;
|
|
227
|
+
|
|
228
|
+
const updateFormat = (name?: string) => {
|
|
229
|
+
const config = data.pages.find((temp: { name: string }) => {
|
|
230
|
+
return temp.name === `${name}`;
|
|
231
|
+
})?.config;
|
|
232
|
+
|
|
233
|
+
fixedWidth = config.width;
|
|
234
|
+
fixedHeight = config.height;
|
|
235
|
+
const width = fixedWidth || iframe?.offsetWidth || 0;
|
|
236
|
+
const height = fixedHeight || iframe?.offsetHeight || 0;
|
|
237
|
+
|
|
238
|
+
const display = document.querySelector("#display-width");
|
|
239
|
+
if(display) {
|
|
240
|
+
display.innerHTML = `${width}x${height}px`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if(iframe) {
|
|
244
|
+
iframe.width = `${width}`;
|
|
245
|
+
iframe.height = `${height}`;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let resizeListener: EventListener;
|
|
250
|
+
|
|
251
|
+
function showTemplate(name: string) {
|
|
252
|
+
if(!iframe) return;
|
|
253
|
+
|
|
254
|
+
const config = data.pages.find((temp: { name: string }) => {
|
|
255
|
+
return temp.name === `${name}`;
|
|
256
|
+
})?.config;
|
|
257
|
+
|
|
258
|
+
console.debug(name, config)
|
|
259
|
+
|
|
260
|
+
updateFormat(name);
|
|
261
|
+
|
|
262
|
+
iframe.src = `/${name}.html`;
|
|
263
|
+
|
|
264
|
+
// Update active state
|
|
265
|
+
document.querySelectorAll('.button').forEach(button => {
|
|
266
|
+
button.classList.remove('active');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const activeButton = document.querySelector(`[data-template="${name}"]`);
|
|
270
|
+
if (activeButton) {
|
|
271
|
+
activeButton.classList.add('active');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
window.removeEventListener("resize", resizeListener);
|
|
275
|
+
|
|
276
|
+
resizeListener = () => updateFormat(name);
|
|
277
|
+
|
|
278
|
+
window.addEventListener("resize", resizeListener);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const template = location.hash.substring(1);
|
|
282
|
+
if (template) {
|
|
283
|
+
showTemplate(template)
|
|
284
|
+
} else {
|
|
285
|
+
const first = data.pages[0]?.name.replace(".astro", "");
|
|
286
|
+
showTemplate(first)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
window.addEventListener("hashchange", () => {
|
|
290
|
+
showTemplate(location.hash.substring(1));
|
|
291
|
+
});
|
|
292
|
+
</script>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<style>
|
|
297
|
+
body {
|
|
298
|
+
font-family: sans-serif, Arial;
|
|
299
|
+
height: 100vh;
|
|
300
|
+
margin: 0;
|
|
301
|
+
overscroll-behavior: none;
|
|
302
|
+
overflow: hidden;
|
|
303
|
+
font-size: 1rem;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
body {
|
|
307
|
+
background-color: #e5e5f7;
|
|
308
|
+
background-image: linear-gradient(#eee 1px, transparent 1px), linear-gradient(to right, #eee 1px, #fff 1px);
|
|
309
|
+
background-size: 20px 20px;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
@media (prefers-color-scheme: dark) {
|
|
313
|
+
body {
|
|
314
|
+
background-color: hsl(258, 7%, 10%);
|
|
315
|
+
color: #fff;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
iframe {
|
|
320
|
+
border: none;
|
|
321
|
+
max-height: 100%;
|
|
322
|
+
max-width: 100%;
|
|
323
|
+
flex: none;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
h1 {
|
|
327
|
+
opacity: 0.65;
|
|
328
|
+
font-size: 0.65rem;
|
|
329
|
+
font-weight: normal;
|
|
330
|
+
text-transform: uppercase;
|
|
331
|
+
margin: 0;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.preview-header {
|
|
335
|
+
display: flex;
|
|
336
|
+
justify-content: center;
|
|
337
|
+
align-items: center;
|
|
338
|
+
padding: 5px 20px;
|
|
339
|
+
font-size: 0.75rem;
|
|
340
|
+
color: #666;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.preview {
|
|
344
|
+
display: grid;
|
|
345
|
+
grid-template-rows: auto 1fr;
|
|
346
|
+
justify-content: center;
|
|
347
|
+
justify-items: center;
|
|
348
|
+
height: 100%;
|
|
349
|
+
margin: 0 20px;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.layout {
|
|
353
|
+
height: 100%;
|
|
354
|
+
display: grid;
|
|
355
|
+
grid-template-columns: 280px 1fr;
|
|
356
|
+
grid-template-rows: 1fr;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.sidebar {
|
|
360
|
+
margin: 8px;
|
|
361
|
+
border-radius: 8px;
|
|
362
|
+
background: #fff;
|
|
363
|
+
padding: 1rem 0.75rem;
|
|
364
|
+
box-sizing: border-box;
|
|
365
|
+
border-right: 1px solid #e0e0e0;
|
|
366
|
+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.sidebar-header {
|
|
370
|
+
display: flex;
|
|
371
|
+
flex-direction: column;
|
|
372
|
+
gap: 1rem;
|
|
373
|
+
margin-bottom: 1rem;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.download-button {
|
|
377
|
+
appearance: none;
|
|
378
|
+
background: #007bff;
|
|
379
|
+
color: white;
|
|
380
|
+
border: none;
|
|
381
|
+
border-radius: 6px;
|
|
382
|
+
padding: 0.5rem 1rem;
|
|
383
|
+
font-size: 0.75rem;
|
|
384
|
+
font-weight: 500;
|
|
385
|
+
cursor: pointer;
|
|
386
|
+
transition: all 0.2s;
|
|
387
|
+
text-transform: uppercase;
|
|
388
|
+
letter-spacing: 0.5px;
|
|
389
|
+
display: flex;
|
|
390
|
+
align-items: center;
|
|
391
|
+
justify-content: center;
|
|
392
|
+
gap: 0.5rem;
|
|
393
|
+
min-height: 2.25rem;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.download-button:hover:not(:disabled) {
|
|
397
|
+
background: #0056b3;
|
|
398
|
+
transform: translateY(-1px);
|
|
399
|
+
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.download-button:disabled {
|
|
403
|
+
opacity: 0.8;
|
|
404
|
+
cursor: not-allowed;
|
|
405
|
+
transform: none;
|
|
406
|
+
box-shadow: none;
|
|
407
|
+
background: #6c757d;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
@keyframes spin {
|
|
411
|
+
0% { transform: rotate(0deg); }
|
|
412
|
+
100% { transform: rotate(360deg); }
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
@media (prefers-color-scheme: dark) {
|
|
416
|
+
.sidebar {
|
|
417
|
+
background-color: hsl(258, 4%, 14%);
|
|
418
|
+
border-right: 1px solid #000000;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
a[href] {
|
|
423
|
+
color: inherit;
|
|
424
|
+
text-decoration: none;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.button,
|
|
428
|
+
button {
|
|
429
|
+
display: inline-block;
|
|
430
|
+
box-sizing: border-box;
|
|
431
|
+
appearance: none;
|
|
432
|
+
width: 100%;
|
|
433
|
+
background: transparent;
|
|
434
|
+
color: inherit;
|
|
435
|
+
display: flex;
|
|
436
|
+
align-items: center;
|
|
437
|
+
justify-content: space-between;
|
|
438
|
+
padding: 0px 4px 0px 6px;
|
|
439
|
+
border: 1px solid #ddd;
|
|
440
|
+
border-radius: 8px;
|
|
441
|
+
margin-bottom: 0.5rem;
|
|
442
|
+
transition: all 0.2s;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.button a[href^="#"] {
|
|
446
|
+
display: flex;
|
|
447
|
+
align-items: center;
|
|
448
|
+
gap: 0.75rem;
|
|
449
|
+
flex: 1;
|
|
450
|
+
min-height: 2rem;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.template-number {
|
|
454
|
+
font-size: 0.75rem;
|
|
455
|
+
opacity: 0.6;
|
|
456
|
+
font-weight: 600;
|
|
457
|
+
min-width: 1.5rem;
|
|
458
|
+
text-align: center;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.template-name {
|
|
462
|
+
flex: 1;
|
|
463
|
+
font-size: 0.8rem;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.button.active {
|
|
467
|
+
background: #007bff;
|
|
468
|
+
color: #fff;
|
|
469
|
+
border-color: #007bff;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.button.active .template-number {
|
|
473
|
+
opacity: 0.9;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.template-link {
|
|
477
|
+
font-size: 0.65rem;
|
|
478
|
+
padding: 8px 10px;
|
|
479
|
+
border-radius: 8px;
|
|
480
|
+
transition: all 0.2s;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.template-link:hover {
|
|
484
|
+
background: #8282823e;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.button:hover,
|
|
488
|
+
button:hover {
|
|
489
|
+
box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.04);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.button:active,
|
|
493
|
+
button:active {
|
|
494
|
+
transition: none;
|
|
495
|
+
box-shadow: none;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
@media (prefers-color-scheme: dark) {
|
|
499
|
+
button {
|
|
500
|
+
border: 1px solid #4b4b4b;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
button:hover {
|
|
504
|
+
background: #3f3f3f;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.button.active {
|
|
508
|
+
background: #0066cc;
|
|
509
|
+
border-color: #0066cc;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.download-button:hover:not(:disabled) {
|
|
513
|
+
background: #0056b3;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.download-button:disabled {
|
|
517
|
+
background: #6c757d;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
</style>
|
|
521
|
+
</body>
|
|
522
|
+
</html>
|
package/index.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import react from "@vitejs/plugin-react";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import * as vite from "vite";
|
|
5
|
+
import child_process from "node:child_process";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {object} options
|
|
9
|
+
* @param {string | ((name: string) => string)} options.filename - The filename to use for the output files. Use `[name]` to insert the page name.
|
|
10
|
+
* @returns {import('astro').AstroIntegration}
|
|
11
|
+
*/
|
|
12
|
+
export default function email(options) {
|
|
13
|
+
return {
|
|
14
|
+
name: "email",
|
|
15
|
+
configureServer(server) {
|
|
16
|
+
server.middlewares.use((req, res, next) => {
|
|
17
|
+
const url = req.url;
|
|
18
|
+
if (!url) return next();
|
|
19
|
+
|
|
20
|
+
// Extract the filename from any sub-path
|
|
21
|
+
const segments = url.split("/").filter(Boolean);
|
|
22
|
+
const filename = segments[segments.length - 1];
|
|
23
|
+
|
|
24
|
+
// Only handle requests for files with extensions
|
|
25
|
+
if (filename?.includes(".")) {
|
|
26
|
+
const publicPath = path.join(process.cwd(), "public", filename);
|
|
27
|
+
|
|
28
|
+
if (fs.existsSync(publicPath)) {
|
|
29
|
+
const stat = fs.statSync(publicPath);
|
|
30
|
+
if (stat.isFile()) {
|
|
31
|
+
res.setHeader("Content-Length", stat.size);
|
|
32
|
+
const stream = fs.createReadStream(publicPath);
|
|
33
|
+
stream.pipe(res);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
next();
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
hooks: {
|
|
43
|
+
"astro:config:setup": ({
|
|
44
|
+
command,
|
|
45
|
+
updateConfig,
|
|
46
|
+
injectRoute,
|
|
47
|
+
addRenderer,
|
|
48
|
+
addDevToolbarApp,
|
|
49
|
+
}) => {
|
|
50
|
+
addRenderer({
|
|
51
|
+
name: "astro-html/react",
|
|
52
|
+
serverEntrypoint: "@astrojs/react/server.js",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
updateConfig({
|
|
56
|
+
vite: {
|
|
57
|
+
plugins: [
|
|
58
|
+
react(),
|
|
59
|
+
optionsPlugin(false), // required for @astrojs/react/server.js to work.
|
|
60
|
+
],
|
|
61
|
+
build: {
|
|
62
|
+
assetsInlineLimit: 1024 * 20, // inline all assets
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
compressHTML: false,
|
|
66
|
+
build: {
|
|
67
|
+
format: "file",
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
injectRoute({
|
|
72
|
+
pattern: "/",
|
|
73
|
+
entrypoint: "astro-html/index.astro",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
addDevToolbarApp("astro-html/toolbar.js");
|
|
77
|
+
},
|
|
78
|
+
"astro:build:done": async ({ dir, pages }) => {
|
|
79
|
+
if (options.filename) {
|
|
80
|
+
const manifest = [];
|
|
81
|
+
|
|
82
|
+
for (const page of pages) {
|
|
83
|
+
const pathname = page.pathname;
|
|
84
|
+
const basename = pathname.split(".")[0];
|
|
85
|
+
|
|
86
|
+
if (!pathname) continue; // index has none
|
|
87
|
+
|
|
88
|
+
const name =
|
|
89
|
+
typeof options.filename === "string"
|
|
90
|
+
? options.filename.replace("[name]", basename)
|
|
91
|
+
: options.filename(basename);
|
|
92
|
+
|
|
93
|
+
fs.renameSync(
|
|
94
|
+
path.resolve(dir.pathname, `${pathname}.html`),
|
|
95
|
+
path.resolve(dir.pathname, name),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const files = [name];
|
|
99
|
+
|
|
100
|
+
// Add all public files
|
|
101
|
+
const publicDir = path.resolve("public");
|
|
102
|
+
if (fs.existsSync(publicDir)) {
|
|
103
|
+
const publicFiles = fs.readdirSync(publicDir);
|
|
104
|
+
for (const publicFile of publicFiles) {
|
|
105
|
+
const publicFilePath = path.resolve(publicDir, publicFile);
|
|
106
|
+
if (fs.statSync(publicFilePath).isFile()) {
|
|
107
|
+
files.push(publicFile);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// check if an .jpg file with the same name exists and copy it to dist too.
|
|
113
|
+
const jpgPath = path.resolve("src/pages", `${pathname}.jpg`);
|
|
114
|
+
if (fs.existsSync(jpgPath)) {
|
|
115
|
+
const newJpgName =
|
|
116
|
+
typeof options.filename === "string"
|
|
117
|
+
? options.filename
|
|
118
|
+
.replace("[name]", basename)
|
|
119
|
+
.replace(/\.[^.]+$/, ".jpg")
|
|
120
|
+
: options.filename(basename).replace(/\.[^.]+$/, ".jpg");
|
|
121
|
+
fs.copyFileSync(jpgPath, path.resolve(dir.pathname, newJpgName));
|
|
122
|
+
files.push(newJpgName);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
manifest.push({
|
|
126
|
+
name: pathname,
|
|
127
|
+
files: files,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fs.writeFileSync(
|
|
132
|
+
path.resolve(dir.pathname, "manifest.json"),
|
|
133
|
+
JSON.stringify(manifest, null, 2),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
"astro:server:setup": ({ server }) => {
|
|
138
|
+
server.ws.on("astro-dev-toolbar:astro-html:toggled", (data) => {
|
|
139
|
+
if (data.state === true) {
|
|
140
|
+
child_process.exec("astro build");
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** @type {(xperimentalReactChildren?: boolean) => vite.Plugin} */
|
|
149
|
+
function optionsPlugin(experimentalReactChildren) {
|
|
150
|
+
const virtualModule = "astro:react:opts";
|
|
151
|
+
const virtualModuleId = `\0${virtualModule}`;
|
|
152
|
+
return {
|
|
153
|
+
name: "astro-html/react:opts",
|
|
154
|
+
resolveId(id) {
|
|
155
|
+
if (id === virtualModule) {
|
|
156
|
+
return virtualModuleId;
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
load(id) {
|
|
160
|
+
if (id === virtualModuleId) {
|
|
161
|
+
return {
|
|
162
|
+
code: `export default {
|
|
163
|
+
experimentalReactChildren: ${JSON.stringify(experimentalReactChildren)}
|
|
164
|
+
}`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "astro-html",
|
|
3
|
+
"description": "Integration for Astro to Generate HTML emails with React (react-email)",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Tim Havlicek",
|
|
7
|
+
"email": "tim.h4vlicek@gmail.com"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/luckydye/astro-html.git"
|
|
12
|
+
},
|
|
13
|
+
"version": "0.19.0",
|
|
14
|
+
"main": "index.js",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"dev": "astro dev",
|
|
17
|
+
"build": "astro build"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@astrojs/react": "^4.4.0",
|
|
21
|
+
"@vitejs/plugin-react": "^4.7.0",
|
|
22
|
+
"@react-email/components": "^0.5.6"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^20.11.17",
|
|
26
|
+
"@types/react": "^18.2.55",
|
|
27
|
+
"@types/react-dom": "^18.2.19"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"Mail.astro",
|
|
31
|
+
"Banner.astro",
|
|
32
|
+
"index.astro",
|
|
33
|
+
"index.js",
|
|
34
|
+
"toolbar.js"
|
|
35
|
+
]
|
|
36
|
+
}
|
package/toolbar.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const icon = `
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="15" height="15" viewBox="0 0 15 15">
|
|
3
|
+
<defs>
|
|
4
|
+
<clipPath id="clip-path">
|
|
5
|
+
<rect id="Rectangle_1688" data-name="Rectangle 1688" width="15" height="15" transform="translate(11726 152)" fill="none"/>
|
|
6
|
+
</clipPath>
|
|
7
|
+
</defs>
|
|
8
|
+
<g id="Mask_Group_4" data-name="Mask Group 4" transform="translate(-11726 -152)" clip-path="url(#clip-path)">
|
|
9
|
+
<g fill="#fff" id="Group_423" data-name="Group 423" transform="translate(0.5)">
|
|
10
|
+
<path id="Path_4183" data-name="Path 4183" d="M14.6,14H7.4A2.4,2.4,0,0,1,5,11.6V8.018H6.2V11.6a1.2,1.2,0,0,0,1.2,1.2h7.2a1.2,1.2,0,0,0,1.2-1.2V8.018H17V11.6A2.4,2.4,0,0,1,14.6,14Z" transform="translate(11722 152)"/>
|
|
11
|
+
<path id="Path_4184" data-name="Path 4184" d="M3.385,9.591V1.927L.963,4.349a.564.564,0,0,1-.8-.8L3.54.176a.564.564,0,0,1,.684-.1h0l.01.006,0,0,.007,0,.006,0,0,0L4.264.1h0a.567.567,0,0,1,.083.069L7.733,3.55a.564.564,0,0,1-.8.8L4.513,1.926V9.591a.564.564,0,1,1-1.128,0Z" transform="translate(11736.949 162.579) rotate(180)"/>
|
|
12
|
+
</g>
|
|
13
|
+
</g>
|
|
14
|
+
</svg>
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
id: 'astro-html',
|
|
19
|
+
name: 'Export emails',
|
|
20
|
+
icon,
|
|
21
|
+
init(canvas, eventTarget) {
|
|
22
|
+
eventTarget.addEventListener('app-toggled', async (event) => {
|
|
23
|
+
if (event.detail.state) {
|
|
24
|
+
setTimeout(() => {eventTarget.dispatchEvent(
|
|
25
|
+
new CustomEvent('toggle-app', {
|
|
26
|
+
detail: { state: false },
|
|
27
|
+
})
|
|
28
|
+
)}, 150)
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
}
|