initial update with nodemailer

This commit is contained in:
Elizabeth Hunt 2024-01-07 01:21:06 -07:00
parent e1905622d7
commit db671d26c4
Signed by: simponic
GPG Key ID: 52B3774857EB24B1
9 changed files with 300 additions and 264 deletions

150
package-lock.json generated
View File

@ -8,14 +8,15 @@
"name": "mistymountains-frontend", "name": "mistymountains-frontend",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@sendgrid/mail": "^7.6.2",
"@supabase/supabase-js": "^1.33.3", "@supabase/supabase-js": "^1.33.3",
"dotenv": "^16.0.0" "dotenv": "^16.0.0",
"nodemailer": "^6.9.8"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "next", "@sveltejs/adapter-auto": "next",
"@sveltejs/adapter-node": "^1.0.0-next.73", "@sveltejs/adapter-node": "^1.0.0-next.73",
"@sveltejs/kit": "next", "@sveltejs/kit": "next",
"@types/nodemailer": "^6.4.14",
"@typescript-eslint/eslint-plugin": "^5.10.1", "@typescript-eslint/eslint-plugin": "^5.10.1",
"@typescript-eslint/parser": "^5.10.1", "@typescript-eslint/parser": "^5.10.1",
"@zerodevx/svelte-toast": "^0.7.1", "@zerodevx/svelte-toast": "^0.7.1",
@ -239,41 +240,6 @@
"node": ">= 8.0.0" "node": ">= 8.0.0"
} }
}, },
"node_modules/@sendgrid/client": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-7.6.2.tgz",
"integrity": "sha512-Yw3i3vPBBwfiIi+4i7+1f1rwQoLlLsu3qW16d1UuRp6RgX6H6yHYb2/PfqwNyCC0qzqIWGUKPWwYe5ggcr5Guw==",
"dependencies": {
"@sendgrid/helpers": "^7.6.2",
"axios": "^0.26.0"
},
"engines": {
"node": "6.* || 8.* || >=10.*"
}
},
"node_modules/@sendgrid/helpers": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-7.6.2.tgz",
"integrity": "sha512-kGW0kM2AOHfXjcvB6Lgwa/nMv8IALu0KyNY9X4HSa3MtLohymuhbG9HgjrOh66+BkbsfA03H3bcT0+sPVJ0GKQ==",
"dependencies": {
"deepmerge": "^4.2.2"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/@sendgrid/mail": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-7.6.2.tgz",
"integrity": "sha512-IHHZFvgU95aqb11AevQvAfautj2pb8iW8UCiUJ2ae9pRF37e6EkBmU9NgdFjbQ/8Xhhm+KDVDzn/JLxDN/GiBw==",
"dependencies": {
"@sendgrid/client": "^7.6.2",
"@sendgrid/helpers": "^7.6.2"
},
"engines": {
"node": "6.* || 8.* || >=10.*"
}
},
"node_modules/@supabase/functions-js": { "node_modules/@supabase/functions-js": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-1.3.3.tgz", "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-1.3.3.tgz",
@ -435,6 +401,15 @@
"integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==", "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==",
"dev": true "dev": true
}, },
"node_modules/@types/nodemailer": {
"version": "6.4.14",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.14.tgz",
"integrity": "sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/phoenix": { "node_modules/@types/phoenix": {
"version": "1.5.4", "version": "1.5.4",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.5.4.tgz", "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.5.4.tgz",
@ -755,14 +730,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/axios": {
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
"dependencies": {
"follow-redirects": "^1.14.8"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -951,14 +918,6 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true "dev": true
}, },
"node_modules/deepmerge": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/detect-indent": { "node_modules/detect-indent": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
@ -1807,25 +1766,6 @@
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
"dev": true "dev": true
}, },
"node_modules/follow-redirects": {
"version": "1.14.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -2317,6 +2257,14 @@
"node-gyp-build-test": "build-test.js" "node-gyp-build-test": "build-test.js"
} }
}, },
"node_modules/nodemailer": {
"version": "6.9.8",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.8.tgz",
"integrity": "sha512-cfrYUk16e67Ks051i4CntM9kshRYei1/o/Gi8K1d+R34OIs21xdFnW7Pt7EucmVKA0LKtqUGNcjMZ7ehjl49mQ==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/normalize-path": { "node_modules/normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@ -3493,32 +3441,6 @@
"picomatch": "^2.2.2" "picomatch": "^2.2.2"
} }
}, },
"@sendgrid/client": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-7.6.2.tgz",
"integrity": "sha512-Yw3i3vPBBwfiIi+4i7+1f1rwQoLlLsu3qW16d1UuRp6RgX6H6yHYb2/PfqwNyCC0qzqIWGUKPWwYe5ggcr5Guw==",
"requires": {
"@sendgrid/helpers": "^7.6.2",
"axios": "^0.26.0"
}
},
"@sendgrid/helpers": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-7.6.2.tgz",
"integrity": "sha512-kGW0kM2AOHfXjcvB6Lgwa/nMv8IALu0KyNY9X4HSa3MtLohymuhbG9HgjrOh66+BkbsfA03H3bcT0+sPVJ0GKQ==",
"requires": {
"deepmerge": "^4.2.2"
}
},
"@sendgrid/mail": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-7.6.2.tgz",
"integrity": "sha512-IHHZFvgU95aqb11AevQvAfautj2pb8iW8UCiUJ2ae9pRF37e6EkBmU9NgdFjbQ/8Xhhm+KDVDzn/JLxDN/GiBw==",
"requires": {
"@sendgrid/client": "^7.6.2",
"@sendgrid/helpers": "^7.6.2"
}
},
"@supabase/functions-js": { "@supabase/functions-js": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-1.3.3.tgz", "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-1.3.3.tgz",
@ -3658,6 +3580,15 @@
"integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==", "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==",
"dev": true "dev": true
}, },
"@types/nodemailer": {
"version": "6.4.14",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.14.tgz",
"integrity": "sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/phoenix": { "@types/phoenix": {
"version": "1.5.4", "version": "1.5.4",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.5.4.tgz", "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.5.4.tgz",
@ -3856,14 +3787,6 @@
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"dev": true "dev": true
}, },
"axios": {
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
"requires": {
"follow-redirects": "^1.14.8"
}
},
"balanced-match": { "balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -4005,11 +3928,6 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true "dev": true
}, },
"deepmerge": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
},
"detect-indent": { "detect-indent": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
@ -4564,11 +4482,6 @@
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
"dev": true "dev": true
}, },
"follow-redirects": {
"version": "1.14.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
},
"fs.realpath": { "fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -4932,6 +4845,11 @@
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.4.0.tgz", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.4.0.tgz",
"integrity": "sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ==" "integrity": "sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ=="
}, },
"nodemailer": {
"version": "6.9.8",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.8.tgz",
"integrity": "sha512-cfrYUk16e67Ks051i4CntM9kshRYei1/o/Gi8K1d+R34OIs21xdFnW7Pt7EucmVKA0LKtqUGNcjMZ7ehjl49mQ=="
},
"normalize-path": { "normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",

View File

@ -16,6 +16,7 @@
"@sveltejs/adapter-auto": "next", "@sveltejs/adapter-auto": "next",
"@sveltejs/adapter-node": "^1.0.0-next.73", "@sveltejs/adapter-node": "^1.0.0-next.73",
"@sveltejs/kit": "next", "@sveltejs/kit": "next",
"@types/nodemailer": "^6.4.14",
"@typescript-eslint/eslint-plugin": "^5.10.1", "@typescript-eslint/eslint-plugin": "^5.10.1",
"@typescript-eslint/parser": "^5.10.1", "@typescript-eslint/parser": "^5.10.1",
"@zerodevx/svelte-toast": "^0.7.1", "@zerodevx/svelte-toast": "^0.7.1",
@ -34,8 +35,8 @@
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@sendgrid/mail": "^7.6.2",
"@supabase/supabase-js": "^1.33.3", "@supabase/supabase-js": "^1.33.3",
"dotenv": "^16.0.0" "dotenv": "^16.0.0",
"nodemailer": "^6.9.8"
} }
} }

3
src/lib/utils/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './setImageUrl';
export * from './mailer';
export * from './retry';

61
src/lib/utils/mailer.ts Normal file
View File

@ -0,0 +1,61 @@
import * as nodemailer from 'nodemailer';
import { continueRetryUntilValidation } from './retry';
export interface Mailer {
sendMail(to: string, subject: string, message: string): Promise<boolean>;
}
export class MistyMountainsMailer implements Mailer {
private from: string;
private domain: string;
private username: string;
private password: string;
private port: number;
private transporter: nodemailer.Transporter;
constructor(username: string, password: string, from: string, domain: string, port: number) {
this.from = from;
this.username = username;
this.password = password;
this.domain = domain;
this.port = port;
this.transporter = nodemailer.createTransport({
host: this.domain,
port: this.port,
auth: {
user: this.username,
pass: this.password
},
requireTLS: true,
tls: {
rejectUnauthorized: true
}
});
}
public async sendMail(to: string, subject: string, message: string) {
const mail = {
from: this.from,
subject,
html: message,
to
};
return !!(await continueRetryUntilValidation<string>(async () => {
const { messageId } = await this.transporter.sendMail(mail);
return messageId;
}));
}
}
export const EnvMistyMountainsMailerFactory = () => {
return new MistyMountainsMailer(
process.env.SMTP_USERNAME,
process.env.SMTP_PASSWORD,
process.env.FROM_EMAIL,
process.env.SMTP_SERVER,
Number(process.env.SMTP_PORT)
);
};

45
src/lib/utils/retry.ts Normal file
View File

@ -0,0 +1,45 @@
export type RetryStrategyF = (retries: number) => number;
export const MAX_DEFAULT_RETRY_AMOUNT = 5;
export const WAIT_MS = 1_000;
export const RETRY_EXPONENT = 2;
export const RETRY_EXPONENTIAL_FACTOR = 1.1;
export const RETRY_JITTER_MAX = 3_000;
const waitFor = (ms: number) => new Promise((res) => setTimeout(res, ms));
const exponentialStrategyWithJitter: RetryStrategyF = (retries: number) =>
WAIT_MS * Math.pow(RETRY_EXPONENT, RETRY_EXPONENTIAL_FACTOR * retries) +
RETRY_JITTER_MAX * Math.random();
export const continueRetryUntilValidation = async <T>(
promiseFn: () => Promise<T> | T | Promise<void> | void,
validationFn: (x: T) => Promise<boolean> | boolean = (x: T) =>
Promise.resolve(!!x),
maxRetries = MAX_DEFAULT_RETRY_AMOUNT,
waitTimeStrategy: RetryStrategyF = exponentialStrategyWithJitter,
retries = 0,
lastError: undefined | unknown = undefined
): Promise<T> => {
if (retries >= maxRetries) {
if (lastError) throw lastError;
throw new Error("Exceeded maximum retry amount");
}
try {
if (retries) await waitFor(waitTimeStrategy(retries));
const res = await promiseFn();
if (res && (await validationFn(res))) return res;
throw new Error("Validation predicate unsuccessful");
} catch (e: unknown) {
return continueRetryUntilValidation(
promiseFn,
validationFn,
maxRetries,
waitTimeStrategy,
retries + 1,
e
);
}
};

View File

@ -1,6 +1,6 @@
import { supabase } from './supabase'; import { supabase } from '../supabase';
const setImageUrl = (imageSpec) => { export const setImageUrl = (imageSpec) => {
const { publicURL, error } = supabase const { publicURL, error } = supabase
.storage .storage
.from('mistymountains') .from('mistymountains')
@ -10,5 +10,3 @@ const setImageUrl = (imageSpec) => {
} }
return imageSpec; return imageSpec;
} }
export default setImageUrl;

View File

@ -1,58 +1,56 @@
import 'dotenv/config'; import 'dotenv/config';
import sgMail from '@sendgrid/mail'; import { EnvMistyMountainsMailerFactory } from '$lib/utils';
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
export async function post({ request }) { export async function post({ request }) {
const body = await request.json(); const body = await request.json();
const { HCAPTCHA_SECRET, FORM_FROM_EMAIL, FORM_TO_EMAIL } = process.env; const { HCAPTCHA_SECRET, FORM_TO_EMAIL } = process.env;
const mailer = EnvMistyMountainsMailerFactory();
const captchaVerified = await fetch(`https://hcaptcha.com/siteverify?response=${body.captchaToken}&secret=${HCAPTCHA_SECRET}`, { const captchaVerified = await fetch(
method: 'POST', `https://hcaptcha.com/siteverify?response=${body.captchaToken}&secret=${HCAPTCHA_SECRET}`,
}) {
.then((res) => res.json()) method: 'POST'
.then((json) => json.success) }
.catch(() => false); )
.then((res) => res.json())
.then((json) => json.success)
.catch(() => false);
if (!captchaVerified) { if (!captchaVerified) {
return { return {
statusCode: 400, statusCode: 400,
body: { body: {
error: 'Captcha verification failed', error: 'Captcha verification failed'
}, }
}; };
} }
const msg = { const text = `Name: ${body.name}
to: FORM_TO_EMAIL, Phone Number: ${body.phone || 'Not Given'}
from: FORM_FROM_EMAIL, Email: ${body.email}
subject: `Form Submission from ${body.name}`, Message: ${body.message}
text: ` `;
Name: ${body.name}
Phone Number: ${body.phone || "Not Given"}
Email: ${body.email}
Message: ${body.message}
`,
};
const messageSent = await sgMail.send(msg) const messageSent = await mailer
.then(() => true) .sendMail(FORM_TO_EMAIL, `Form Submission from ${body.name}`, text)
.catch((error) => { .then(() => true)
console.error(error); .catch((error) => {
return false; console.error(error);
}); return false;
});
if (!messageSent) { if (!messageSent) {
return { return {
statusCode: 500, statusCode: 500,
body: { body: {
error: 'Message could not be sent', error: 'Message could not be sent'
}, }
}; };
} }
return { return {
body: { body: {
success: true, success: true
}, }
}; };
} }

View File

@ -1,73 +1,82 @@
<script>
import { setImageUrl } from '$lib/utils';
import { Navigation, Pagination, Scrollbar, A11y, Autoplay } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/svelte';
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
import 'swiper/css/scrollbar';
import 'swiper/css/autoplay';
const images = [
{ image: 'mountains.png', alt: 'Cloudy mountains in a light sky' },
{ image: 'office-1-1.jpeg', alt: 'Office room one' },
{ image: 'office-1-2.jpeg', alt: 'Office room one angle two' }
].map(setImageUrl);
</script>
<div class="row align-items-center py-2"> <div class="row align-items-center py-2">
<div class="col-md-7"> <div class="col-md-7">
<Swiper <Swiper
modules={[Navigation, Pagination, Scrollbar, A11y, Autoplay]} modules={[Navigation, Pagination, Scrollbar, A11y, Autoplay]}
class="rounded shadow" class="rounded shadow"
spaceBetween={50} spaceBetween={50}
slidesPerView={1} slidesPerView={1}
autoHeight={true} autoHeight={true}
loop={true} loop={true}
navigation navigation
pagination={{ clickable: true }} pagination={{ clickable: true }}
autoplay={true} autoplay={true}
speed={1000} speed={1000}
on:slideChange={() => console.log('slide change')} on:slideChange={() => console.log('slide change')}
on:swiper={(e) => console.log(e.detail[0])} on:swiper={(e) => console.log(e.detail[0])}
> >
{#each images as { image, alt } (image)} {#each images as { image, alt } (image)}
<SwiperSlide> <SwiperSlide>
<img class="image-fluid" style="width:100%" src={image} alt={alt} /> <img class="image-fluid" style="width:100%" src={image} {alt} />
</SwiperSlide> </SwiperSlide>
{/each} {/each}
</Swiper> </Swiper>
</div> </div>
<div class="col-md-5 my-2"> <div class="col-md-5 my-2">
<h2>Helping you conquer Mount Doom</h2> <h2>Helping you conquer Mount Doom</h2>
<p><a href="/contact" class="btn btn-success shadow">FREE 15 Minute Consultation</a></p> <p><a href="/contact" class="btn btn-success shadow">FREE 15 Minute Consultation</a></p>
</div> </div>
</div> </div>
<div class="bg-light rounded p-3 shadow"> <div class="bg-light rounded p-3 shadow">
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<div class="col-md-8 mt-2 border-bottom"> <div class="col-md-8 mt-2 border-bottom">
<h3>"Darkness must pass, a new day will come, and when the sun shines, it will shine out the clearer."</h3> <h3>
<div class="d-flex justify-content-end"> "Darkness must pass, a new day will come, and when the sun shines, it will shine out the
- Samwise Gamgee clearer."
</div> </h3>
<br> <div class="d-flex justify-content-end">- Samwise Gamgee</div>
<p> <br />
Misty Mountains Therapy is a privately owned, high quality, specialty therapy clinic, founded in January 2020 by Jefferson Hunt. We are dedicated to providing comprehensive therapy evaluation and treatment services to children and adults for a wide variety of disorders in the most efficient and effective manner possible in the Rexburg area. We believe that therapy should be fun, engaging, and most importantly, useful to our clients. <p>
</p> Misty Mountains Therapy is a privately owned, high quality, specialty therapy clinic,
<p> founded in January 2020 by Jefferson Hunt. We are dedicated to providing comprehensive
We are currently accepting new clients and offer a variety of services to help you live the life you desire. To find the right fit for you, schedule a <a href="/contact">free 15-minute consultation</a>. therapy evaluation and treatment services to children and adults for a wide variety of
</p> disorders in the most efficient and effective manner possible in the Rexburg area. We
</div> believe that therapy should be fun, engaging, and most importantly, useful to our clients.
</div> </p>
<p>
We are currently accepting new clients and offer a variety of services to help you live the
life you desire. To find the right fit for you, schedule a <a href="/contact"
>free 15-minute consultation</a
>.
</p>
</div>
</div>
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<div class="col-md-8 mt-2"> <div class="col-md-8 mt-2">
<div> <div>
We do not have a crisis line. If you or someone you know is in danger please call 911, visit your nearest emergency room, call the National Suicide Prevention Lifeline for free crisis counseling at <a href="tel:18002738255">(800)273-TALK</a> (8255), or text HELLO to 741-741. We do not have a crisis line. If you or someone you know is in danger please call 911, visit
</div> your nearest emergency room, call the National Suicide Prevention Lifeline for free crisis
</div> counseling at <a href="tel:18002738255">(800)273-TALK</a> (8255), or text HELLO to 741-741.
</div> </div>
<br> </div>
</div>
<br />
</div> </div>
<script>
import { supabase } from '$lib/supabase';
import setImageUrl from '$lib/utils';
import { Navigation, Pagination, Scrollbar, A11y, Autoplay } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/svelte';
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
import 'swiper/css/scrollbar';
import 'swiper/css/autoplay';
const images = [
{ image: 'mountains.png', alt: 'Cloudy mountains in a light sky' },
{ image: 'office-1-1.jpeg', alt: 'Office room one' },
{ image: 'office-1-2.jpeg', alt: 'Office room one angle two' },
].map(setImageUrl);
</script>

View File

@ -1,35 +1,38 @@
<script> <script>
import DirectionCard from '../../components/DirectionCard.svelte'; import DirectionCard from '../../components/DirectionCard.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { supabase } from '$lib/supabase'; import { supabase } from '$lib/supabase';
import setImageUrl from '$lib/utils'; import { setImageUrl } from '$lib/utils';
const getPeople = async () => { const getPeople = async () => {
const { data, error } = await supabase.from('people').select().order('id'); const { data, error } = await supabase.from('people').select().order('id');
if (!error) { if (!error) {
return data; return data;
} }
return []; return [];
} };
let people = []; let people = [];
onMount(async () => { onMount(async () => {
people = await getPeople().then((people) => people.map(setImageUrl)); people = await getPeople().then((people) => people.map(setImageUrl));
}); });
</script> </script>
<main> <main>
<h1 class="text-center">Our Team</h1> <h1 class="text-center">Our Team</h1>
{#if people.length} {#if people.length}
{#each people as person, i} {#each people as person, i}
<div class="row border-bottom"> <div class="row border-bottom">
<DirectionCard imageSpec={{image: person.image, alt: person.name}} direction={i % 2 ? 'left' : 'right'}> <DirectionCard
<h2>{person.name}, {person.position}</h2> imageSpec={{ image: person.image, alt: person.name }}
<p style="white-space: pre-line">{person.bio}</p> direction={i % 2 ? 'left' : 'right'}
<a href="mailto:{person.email}"><p>{person.email}</p></a> >
</DirectionCard> <h2>{person.name}, {person.position}</h2>
</div> <p style="white-space: pre-line">{person.bio}</p>
{/each} <a href="mailto:{person.email}"><p>{person.email}</p></a>
{/if} </DirectionCard>
</div>
{/each}
{/if}
</main> </main>