Add a contact form to the resume #1

Merged
vato007 merged 18 commits from contact-form into main 2025-02-03 18:10:13 +10:30
11 changed files with 2426 additions and 38 deletions
Showing only changes of commit d487f7ecb7 - Show all commits

View File

@@ -16,6 +16,8 @@ jobs:
with: with:
lfs: true lfs: true
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with:
node-version: 20
- name: Install pico - name: Install pico
run: npm ci run: npm ci
@@ -30,6 +32,13 @@ jobs:
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy build --project-name=resume 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 - uses: actions/upload-artifact@v3
with: with:
name: Website name: Website

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
/node_modules node_modules
.DS_Store .DS_Store
.vscode .vscode
build build

2286
contact-email-worker/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
{
"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"
}
}

View File

@@ -0,0 +1,18 @@
import { EmailMessage } from "cloudflare:email";
import { WorkerEntrypoint } from "cloudflare:workers";
export default class SendEmailWorker extends WorkerEntrypoint<Env> {
async sendEmail(rawMessage: string): Promise<Response> {
try {
const cfMessage = new EmailMessage(
"contact@michaelpivato.dev",
"contact@michaelpivato.dev",
rawMessage
);
await this.env.SEB.send(cfMessage);
} catch (e) {
return new Response((e as Error).message);
}
return new Response();
}
}

View 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"]
}

View 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;
}

View File

@@ -0,0 +1,13 @@
{
"$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
}

View File

@@ -1,9 +1,16 @@
import staticFormsPlugin from "@cloudflare/pages-plugin-static-forms"; import staticFormsPlugin from "@cloudflare/pages-plugin-static-forms";
import { EmailMessage } from "cloudflare:email";
interface SendEmailWorker {
sendEmail(rawMessage: string): Promise<Response>;
}
interface Env {
SERVICE: SendEmailWorker;
}
const formatEmptyString = (s: string) => s ?? "Not Specified"; const formatEmptyString = (s: string) => s ?? "Not Specified";
export const onRequest: PagesFunction = (context) => { export const onRequest: PagesFunction<Env> = (context) => {
// Wrap static forms plugin so we can extract the env to use email routing // Wrap static forms plugin so we can extract the env to use email routing
return staticFormsPlugin({ return staticFormsPlugin({
respondWith: async ({ formData }) => { respondWith: async ({ formData }) => {
@@ -15,38 +22,25 @@ export const onRequest: PagesFunction = (context) => {
// Must have some kind of identifiable information for me to actually care about them. // Must have some kind of identifiable information for me to actually care about them.
if ((fullName || email) && message) { if ((fullName || email) && message) {
const rawEmailMessage = `----
From: Michael Pivato Contact Form <contact@michaelpivato.dev>
To: Michael Pivato <contact@michaelpivato.dev
Subject: Message from ${fullName ?? email}
You've received a new message from ${fullName ?? email}.
Full Name: ${formatEmptyString(fullName)}
Organisation: ${formatEmptyString(organisation)}
Email: ${formatEmptyString(email)}
Mobile: ${formatEmptyString(mobile)}
Message:
${message}
----`;
const cfMessage = new EmailMessage(
"contact@michaelpivato.dev",
"contact@michaelpivato.dev",
rawEmailMessage
);
try { try {
await (context.env as any).SEB.send(cfMessage); const rawEmailMessage = `----
} catch (e) { From: Michael Pivato Contact Form <contact@michaelpivato.dev>
return new Response(e.message); To: Michael Pivato <contact@michaelpivato.dev
} Subject: Message from ${fullName ?? email}
console.log("Full Name: " + fullName ?? "fullname"); You've received a new message from ${fullName ?? email}.
console.log("Organisation: " + organisation); Full Name: ${formatEmptyString(fullName)}
console.log("Email: " + email ?? "email"); Organisation: ${formatEmptyString(organisation)}
console.log("Mobile: " + mobile ?? "mobile"); Email: ${formatEmptyString(email)}
console.log("Message: " + message); Mobile: ${formatEmptyString(mobile)}
Message:
${message}
----`;
await context.env.SERVICE.sendEmail(rawEmailMessage);
} catch (e) {
return new Response(e);
}
} }
return Response.redirect("https://michaelpivato.dev"); return Response.redirect("https://michaelpivato.dev");

6
wrangler.json Normal file
View File

@@ -0,0 +1,6 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "resume",
"compatibility_date": "2025-01-29",
"services": [{ "binding": "SERVICE", "service": "contact-email" }]
}

View File

@@ -1,6 +0,0 @@
send_email = [
{name = "SEB", destination_address = "contact@michaelpivato.dev"},
]
compatibility_flags = [ "nodejs_compat" ]
compatibility_date = "2024-09-23"