Add SSR contact form
All checks were successful
Build and Deploy to Web Server / deploy (push) Successful in 18m1s
All checks were successful
Build and Deploy to Web Server / deploy (push) Successful in 18m1s
This commit is contained in:
@@ -14,6 +14,8 @@ import robotsTxt from "astro-robots-txt";
|
||||
|
||||
import { transformerMetaHighlight } from "@shikijs/transformers";
|
||||
|
||||
import node from "@astrojs/node";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
@@ -51,12 +53,15 @@ export default defineConfig({
|
||||
},
|
||||
|
||||
site: "https://www.nathancummins.com.au",
|
||||
|
||||
trailingSlash: "always",
|
||||
|
||||
image: {
|
||||
responsiveStyles: false
|
||||
},
|
||||
|
||||
output: "static"
|
||||
});
|
||||
output: "static",
|
||||
|
||||
adapter: node({
|
||||
mode: "standalone"
|
||||
})
|
||||
});
|
1650
package-lock.json
generated
1650
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.3.1",
|
||||
"@astrojs/node": "^9.4.3",
|
||||
"@astrojs/sitemap": "^3.4.2",
|
||||
"@iconify-json/fa": "^1.2.1",
|
||||
"@iconify-json/fa7-brands": "^1.2.0",
|
||||
@@ -22,6 +23,7 @@
|
||||
"astro-robots-txt": "^1.0.0",
|
||||
"astro-seo-schema": "^5.1.0",
|
||||
"get-audio-duration": "^4.0.1",
|
||||
"nodemailer": "^7.0.5",
|
||||
"schema-dts": "^1.1.5",
|
||||
"tailwindcss": "^4.1.11"
|
||||
},
|
||||
@@ -29,6 +31,7 @@
|
||||
"@cspell/dict-en-au": "^1.1.4",
|
||||
"@shikijs/transformers": "^3.9.2",
|
||||
"@types/howler": "^2.2.12",
|
||||
"@types/nodemailer": "^7.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
||||
"@typescript-eslint/parser": "^8.38.0",
|
||||
"eslint": "^9.32.0",
|
||||
|
102
src/components/ContactForm.astro
Normal file
102
src/components/ContactForm.astro
Normal file
@@ -0,0 +1,102 @@
|
||||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
|
||||
const ID = `contact-form-${Math.random().toString(36)}`;
|
||||
---
|
||||
|
||||
<form
|
||||
class="mx-auto mt-8 max-w-xl space-y-6 rounded bg-white p-8 shadow-lg dark:bg-gray-900"
|
||||
id={ID}
|
||||
>
|
||||
<div>
|
||||
<label for="name" class="block font-bold">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
required
|
||||
class="mt-2 block w-full rounded p-2 shadow dark:bg-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="email" class="block font-bold">Your email</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
required
|
||||
class="mt-2 block w-full rounded p-2 shadow dark:bg-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="subject" class="block font-bold">Subject</label>
|
||||
<input
|
||||
type="text"
|
||||
id="subject"
|
||||
name="subject"
|
||||
class="mt-2 block w-full rounded p-2 shadow dark:bg-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="message" class="block font-bold">Message</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
rows="4"
|
||||
class="mt-2 block w-full rounded p-2 shadow dark:bg-gray-600"></textarea>
|
||||
</div>
|
||||
<button
|
||||
id="submit"
|
||||
type="submit"
|
||||
class="bg-primary text-md font-header hover:text-primary repeat hover:ring-primary mx-auto inline-block rounded px-6 py-3 font-light text-white uppercase drop-shadow transition hover:bg-white hover:ring-2"
|
||||
><Icon
|
||||
name="mdi:loading"
|
||||
id="loading-icon"
|
||||
class="mr-4 hidden animate-spin"
|
||||
/><span id="response-message" class="inline">Send message</span></button
|
||||
>
|
||||
</form>
|
||||
|
||||
<script define:vars={{ ID }}>
|
||||
document.addEventListener("astro:page-load", () => {
|
||||
const form = document.getElementById(ID);
|
||||
const responseMsg = form.querySelector("#response-message");
|
||||
const button = form.querySelector("#submit");
|
||||
const loadingIcon = form.querySelector("#loading-icon");
|
||||
|
||||
form.addEventListener("submit", async function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
loadingIcon.classList.replace("hidden", "inline");
|
||||
responseMsg.textContent = "Sending...";
|
||||
button.disabled = true;
|
||||
|
||||
try {
|
||||
const formData = new FormData(form);
|
||||
const response = await fetch("/api/email/contact/", {
|
||||
method: "POST",
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
responseMsg.textContent = data.message;
|
||||
form.reset();
|
||||
} else {
|
||||
responseMsg.textContent = data.message;
|
||||
}
|
||||
} catch (err) {
|
||||
responseMsg.textContent =
|
||||
"An error occurred. Please refresh the page and try again.";
|
||||
}
|
||||
|
||||
loadingIcon.classList.replace("inline", "hidden");
|
||||
|
||||
setTimeout(() => {
|
||||
responseMsg.textContent = "Send message";
|
||||
button.disabled = false;
|
||||
}, 10000);
|
||||
});
|
||||
});
|
||||
</script>
|
13
src/env.d.ts
vendored
13
src/env.d.ts
vendored
@@ -12,3 +12,16 @@ interface Window {
|
||||
player: any;
|
||||
playerIsInitialised: boolean;
|
||||
}
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly EMAIL_USER: string;
|
||||
readonly EMAIL_PASS: string;
|
||||
readonly EMAIL_PORT: number;
|
||||
readonly EMAIL_SECURE: "true" | "false";
|
||||
readonly EMAIL_HOST: string;
|
||||
readonly EMAIL_FROM: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
46
src/lib/email.ts
Normal file
46
src/lib/email.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { createTransport } from "nodemailer";
|
||||
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||
|
||||
import type { Options } from "nodemailer/lib/mailer";
|
||||
|
||||
interface Email {
|
||||
email: string;
|
||||
message: string;
|
||||
html?: string;
|
||||
subject: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function constructEmailHtml(props: Email) {
|
||||
return `<div>${props.message}</div>`;
|
||||
}
|
||||
|
||||
export default async function (props: Email) {
|
||||
const transporter = createTransport({
|
||||
host: process.env.EMAIL_HOST || import.meta.env.EMAIL_HOST,
|
||||
port: process.env.EMAIL_PORT || import.meta.env.EMAIL_PORT,
|
||||
secure:
|
||||
process.env.EMAIL_SECURE === "true" ||
|
||||
import.meta.env.EMAIL_SECURE === "true",
|
||||
auth: {
|
||||
user: process.env.EMAIL_USER || import.meta.env.EMAIL_USER,
|
||||
pass: process.env.EMAIL_PASS || import.meta.env.EMAIL_PASS
|
||||
}
|
||||
} as SMTPTransport.Options);
|
||||
|
||||
if (!props.html) {
|
||||
props.html = constructEmailHtml(props);
|
||||
}
|
||||
|
||||
const message: Options = {
|
||||
from: `${props.name} (Website) <${process.env.EMAIL_FROM || import.meta.env.EMAIL_FROM}>`,
|
||||
to: "nathan@nathancummins.com.au",
|
||||
replyTo: props.email,
|
||||
subject: props.subject,
|
||||
text: props.message,
|
||||
html: props.html
|
||||
};
|
||||
|
||||
const info = await transporter.sendMail(message);
|
||||
return info;
|
||||
}
|
52
src/pages/api/email/contact.ts
Normal file
52
src/pages/api/email/contact.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import sendEmail from "@lib/email";
|
||||
import type { APIRoute } from "astro";
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
const data = await request.formData();
|
||||
const email = data.get("email") as string | null;
|
||||
const subject = data.get("subject") as string | null;
|
||||
const message = data.get("message") as string | null;
|
||||
const name = data.get("name") as string | null;
|
||||
|
||||
if (!email || !subject || !message || !name) {
|
||||
return new Response(
|
||||
JSON.stringify({ message: "Required field missing." }),
|
||||
{
|
||||
status: 400,
|
||||
statusText: "Missing required fields."
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const html = `<h1>Message Received via Contact Form</h1><br>
|
||||
<b>Name</b>: ${name}<br>
|
||||
<b>Email</b>: ${email}<br>
|
||||
<b>Message</b>: ${message}`;
|
||||
|
||||
try {
|
||||
await sendEmail({ email, subject, message, name, html });
|
||||
} catch (error) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
message: `Message failed to send with an internal server error. Please contact me directly via a method below.`
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
statusText: `Message failed to send with an internal server error. Please contact me directly via a method below.`
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
message:
|
||||
"Thanks for getting in touch! I will try to get back to you as soon as I can."
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
statusText: "OK"
|
||||
}
|
||||
);
|
||||
};
|
@@ -1,16 +1,25 @@
|
||||
---
|
||||
import ContactForm from "@components/ContactForm.astro";
|
||||
import Paragraph from "@components/Paragraph.astro";
|
||||
import SectionTitle from "@components/SectionTitle.astro";
|
||||
import TextLink from "@components/TextLink.astro";
|
||||
import MainLayout from "@layouts/MainLayout.astro";
|
||||
import { Icon } from "astro-icon/components";
|
||||
import Paragraph from "../components/Paragraph.astro";
|
||||
import SectionTitle from "../components/SectionTitle.astro";
|
||||
import TextLink from "../components/TextLink.astro";
|
||||
import MainLayout from "../layouts/MainLayout.astro";
|
||||
---
|
||||
|
||||
<MainLayout title="Contact">
|
||||
<div class="w-full">
|
||||
<section id="contact" class="bg-white dark:bg-gray-950">
|
||||
<section id="contact" class="bg-primary dark:bg-gray-800">
|
||||
<div class="mx-auto max-w-4xl px-8 py-16 text-center">
|
||||
<SectionTitle>Let's get in touch!</SectionTitle>
|
||||
<SectionTitle class="text-white" lineColour="text-white"
|
||||
>Let's get in touch!</SectionTitle
|
||||
>
|
||||
<ContactForm />
|
||||
</div>
|
||||
</section>
|
||||
<section id="details" class="bg-white dark:bg-gray-950">
|
||||
<div class="mx-auto max-w-4xl px-8 py-16 text-center">
|
||||
<SectionTitle>Other ways to reach me</SectionTitle>
|
||||
<div class="grid grid-cols-1 text-lg md:grid-cols-2">
|
||||
<div class="col-span-1 flex flex-col items-center justify-center p-4">
|
||||
<Icon name="fa7-solid:at" class="text-4xl" />
|
||||
|
Reference in New Issue
Block a user