Compare commits
14 Commits
cards
...
c409459e27
| Author | SHA1 | Date | |
|---|---|---|---|
| c409459e27 | |||
| 132360adc2 | |||
| 4d80cef491 | |||
| 23f0d518c5 | |||
| b543b57b4e | |||
| 776088d62a | |||
| ffa8ceb563 | |||
| 174996d572 | |||
| 8b5dfc68f3 | |||
| d487f7ecb7 | |||
| 73a4ee7df4 | |||
| 154ad1d9ea | |||
| 8cf55aa0eb | |||
| f2227f673e |
@@ -2,7 +2,6 @@ name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
@@ -17,6 +16,8 @@ jobs:
|
||||
with:
|
||||
lfs: true
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install pico
|
||||
run: npm ci
|
||||
@@ -25,12 +26,18 @@ jobs:
|
||||
run: ./build.sh
|
||||
|
||||
- name: Publish to Cloudflare Pages
|
||||
uses: cloudflare/pages-action@v1
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: resume
|
||||
directory: build
|
||||
command: pages deploy build --project-name=resume
|
||||
|
||||
- name: Publish Email Worker
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
workingDirectory: contact-email-worker
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
/node_modules
|
||||
node_modules
|
||||
.DS_Store
|
||||
.vscode
|
||||
build
|
||||
.wrangler
|
||||
3
build.sh
3
build.sh
@@ -1,5 +1,6 @@
|
||||
mkdir -p build/@picocss/pico/css/
|
||||
cp *.png *.xml *.svg *.css *.webmanifest *.ico robots.txt _headers build
|
||||
cp -r *.png *.xml *.svg *.css *.webmanifest *.ico robots.txt _headers functions contact build
|
||||
# https://github.com/cloudflare/workers-sdk/issues/3615
|
||||
sed 's/node_modules\///' index.html > build/index.html
|
||||
sed 's/node_modules\///' contact/index.html > build/contact/index.html
|
||||
cp node_modules/@picocss/pico/css/pico.min.css build/@picocss/pico/css/
|
||||
2446
contact-email-worker/package-lock.json
generated
Normal file
2446
contact-email-worker/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
contact-email-worker/package.json
Normal file
20
contact-email-worker/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "royal-leaf-c03c",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"deploy": "wrangler deploy",
|
||||
"dev": "wrangler dev",
|
||||
"start": "wrangler dev",
|
||||
"cf-typegen": "wrangler types"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/vitest-pool-workers": "^0.6.4",
|
||||
"@cloudflare/workers-types": "^4.20250129.0",
|
||||
"typescript": "^5.5.2",
|
||||
"wrangler": "^3.107.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"mimetext": "^3.0.27"
|
||||
}
|
||||
}
|
||||
56
contact-email-worker/src/index.ts
Normal file
56
contact-email-worker/src/index.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { EmailMessage } from "cloudflare:email";
|
||||
import { WorkerEntrypoint } from "cloudflare:workers";
|
||||
import { createMimeMessage } from "mimetext";
|
||||
|
||||
const formatEmptyString = (s: string) => s ?? "Not Specified";
|
||||
|
||||
interface EmailDetails {
|
||||
fullName: string;
|
||||
organisation: string;
|
||||
email: string;
|
||||
mobile: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export default class SendEmailWorker extends WorkerEntrypoint<Env> {
|
||||
async fetch() {
|
||||
return new Response("Unimplemented");
|
||||
}
|
||||
|
||||
async sendEmail({
|
||||
fullName,
|
||||
organisation,
|
||||
email,
|
||||
mobile,
|
||||
message,
|
||||
}: EmailDetails) {
|
||||
const msg = createMimeMessage();
|
||||
msg.setSender({
|
||||
name: "Michael Pivato Contact Form",
|
||||
addr: "contact@michaelpivato.dev",
|
||||
});
|
||||
msg.setRecipient("contact@michaelpivato.dev");
|
||||
msg.setSubject(`Message from ${fullName ?? email}`);
|
||||
msg.addMessage({
|
||||
contentType: "text/plain",
|
||||
data: `You've received a new message from ${fullName ?? email}.
|
||||
Full Name: ${formatEmptyString(fullName)}
|
||||
Organisation: ${formatEmptyString(organisation)}
|
||||
Email: ${formatEmptyString(email)}
|
||||
Mobile: ${formatEmptyString(mobile)}
|
||||
|
||||
Message:
|
||||
${message}`,
|
||||
});
|
||||
try {
|
||||
const cfMessage = new EmailMessage(
|
||||
"contact@michaelpivato.dev",
|
||||
"contact@michaelpivato.dev",
|
||||
msg.asRaw()
|
||||
);
|
||||
await this.env.SEB.send(cfMessage);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
46
contact-email-worker/tsconfig.json
Normal file
46
contact-email-worker/tsconfig.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
"target": "es2021",
|
||||
/* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
"lib": ["es2021"],
|
||||
/* Specify what JSX code is generated. */
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Specify what module code is generated. */
|
||||
"module": "es2022",
|
||||
/* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
"moduleResolution": "Bundler",
|
||||
/* Specify type package names to be included without being referenced in a source file. */
|
||||
"types": [
|
||||
"@cloudflare/workers-types/2023-07-01"
|
||||
],
|
||||
/* Enable importing .json files */
|
||||
"resolveJsonModule": true,
|
||||
|
||||
/* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
|
||||
"allowJs": true,
|
||||
/* Enable error reporting in type-checked JavaScript files. */
|
||||
"checkJs": false,
|
||||
|
||||
/* Disable emitting files from a compilation. */
|
||||
"noEmit": true,
|
||||
|
||||
/* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
"isolatedModules": true,
|
||||
/* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"allowSyntheticDefaultImports": true,
|
||||
/* Ensure that casing is correct in imports. */
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
/* Enable all strict type-checking options. */
|
||||
"strict": true,
|
||||
|
||||
/* Skip type checking all .d.ts files. */
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"exclude": ["test"],
|
||||
"include": ["worker-configuration.d.ts", "src/**/*.ts"]
|
||||
}
|
||||
5
contact-email-worker/worker-configuration.d.ts
vendored
Normal file
5
contact-email-worker/worker-configuration.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
// Generated by Wrangler
|
||||
// After adding bindings to `wrangler.json`, regenerate this interface via `npm run cf-typegen`
|
||||
interface Env {
|
||||
SEB: SendEmail;
|
||||
}
|
||||
14
contact-email-worker/wrangler.json
Normal file
14
contact-email-worker/wrangler.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "contact-email",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-01-29",
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
"send_email": [
|
||||
{ "name": "SEB", "destination_address": "contact@michaelpivato.dev" }
|
||||
],
|
||||
"workers_dev": false,
|
||||
"compatibility_flags": ["nodejs_compat"]
|
||||
}
|
||||
96
contact/index.html
Normal file
96
contact/index.html
Normal file
@@ -0,0 +1,96 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Michael Pivato | Contact</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="../node_modules/@picocss/pico/css/pico.min.css"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="../apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="../favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="../favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="../site.webmanifest" />
|
||||
<link rel="mask-icon" href="../safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
content="#13171f"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: light)"
|
||||
content="#2a3140"
|
||||
/>
|
||||
<meta name="description" content="Michael Pivato's Resume: Contact Form" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<header>
|
||||
<hgroup>
|
||||
<h1>Michael Pivato</h1>
|
||||
<p>Send Michael a message</p>
|
||||
<a href="../">Back to resume</a>
|
||||
</hgroup>
|
||||
</header>
|
||||
<form data-static-form-name="contact">
|
||||
<fieldset>
|
||||
<label>
|
||||
Name
|
||||
<input name="name" placeholder="Name" autocomplete="name" />
|
||||
</label>
|
||||
<label
|
||||
>Organisation
|
||||
<input
|
||||
name="org"
|
||||
placeholder="Organisation"
|
||||
autocomplete="organization"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Email
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Email"
|
||||
autocomplete="email"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Mobile
|
||||
<input
|
||||
type="tel"
|
||||
name="mobile"
|
||||
placeholder="Mobile"
|
||||
autocomplete="mobile"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Message
|
||||
<textarea name="message" placeholder="Mesage..."></textarea>
|
||||
</label>
|
||||
</fieldset>
|
||||
<input type="submit" value="Send Message" />
|
||||
</form>
|
||||
<footer class="container">
|
||||
<small>Michael Pivato • 2025</small>
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
47
functions/contact.ts
Normal file
47
functions/contact.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import staticFormsPlugin from "@cloudflare/pages-plugin-static-forms";
|
||||
|
||||
interface EmailDetails {
|
||||
fullName: string;
|
||||
organisation: string;
|
||||
email: string;
|
||||
mobile: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface SendEmailWorker {
|
||||
sendEmail(rawMessage: EmailDetails): Promise<Response>;
|
||||
}
|
||||
|
||||
interface Env {
|
||||
SERVICE: SendEmailWorker;
|
||||
}
|
||||
|
||||
export const onRequest: PagesFunction<Env> = (context) => {
|
||||
// Wrap static forms plugin so we can extract the env to use email routing
|
||||
return staticFormsPlugin({
|
||||
respondWith: async ({ formData }) => {
|
||||
const fullName = formData.get("name");
|
||||
const organisation = formData.get("org");
|
||||
const email = formData.get("email");
|
||||
const mobile = formData.get("mobile");
|
||||
const message = formData.get("message");
|
||||
|
||||
// Must have some kind of identifiable information for me to actually care about them.
|
||||
if ((fullName || email) && message) {
|
||||
try {
|
||||
await context.env.SERVICE.sendEmail({
|
||||
fullName,
|
||||
organisation,
|
||||
email,
|
||||
mobile,
|
||||
message,
|
||||
});
|
||||
} catch (e) {
|
||||
return new Response(e);
|
||||
}
|
||||
}
|
||||
|
||||
return Response.redirect("https://michaelpivato.dev");
|
||||
},
|
||||
})(context);
|
||||
};
|
||||
13
functions/tsconfig.json
Normal file
13
functions/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "nodenext",
|
||||
"lib": [
|
||||
"esnext"
|
||||
],
|
||||
"types": [
|
||||
"@cloudflare/workers-types"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,7 @@
|
||||
<hgroup>
|
||||
<h1>Michael Pivato</h1>
|
||||
<p>Career summary and interests</p>
|
||||
<a href="contact">Contact</a>
|
||||
</hgroup>
|
||||
</header>
|
||||
<p>
|
||||
|
||||
1092
package-lock.json
generated
1092
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -7,9 +7,17 @@
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"type": "module",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@picocss/pico": "^2.0.0"
|
||||
"@cloudflare/pages-plugin-static-forms": "^1.0.3",
|
||||
"@picocss/pico": "^2.0.0",
|
||||
"mimetext": "^3.0.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20250129.0",
|
||||
"typescript": "^5.7.3",
|
||||
"wrangler": "^3.107.2"
|
||||
}
|
||||
}
|
||||
|
||||
7
wrangler.json
Normal file
7
wrangler.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "resume",
|
||||
"compatibility_date": "2025-01-29",
|
||||
"services": [{ "binding": "SERVICE", "service": "contact-email" }],
|
||||
"pages_build_output_dir": "build"
|
||||
}
|
||||
Reference in New Issue
Block a user