use maizzle for email templating, starting with reset password email
This commit is contained in:
parent
3f279634b6
commit
514dae3ebb
@ -84,7 +84,7 @@ export function AuthForm<S extends z.ZodType<any, any>>({
|
||||
type="submit"
|
||||
disabled={ctx.formState.isSubmitting}
|
||||
className={clsx(
|
||||
"w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white focus:outline-none focus:border-primary-700 focus:shadow-outline-primary transition duration-150 ease-in-out",
|
||||
"w-full flex justify-center py-2 px-4 border border-transparent text-base font-medium rounded-md text-white focus:outline-none focus:border-primary-700 focus:shadow-outline-primary transition duration-150 ease-in-out",
|
||||
{
|
||||
"bg-primary-400 cursor-not-allowed": ctx.formState.isSubmitting,
|
||||
"bg-primary-600 hover:bg-primary-700": !ctx.formState.isSubmitting,
|
||||
|
@ -4,7 +4,7 @@ import db, { User } from "../../../db";
|
||||
import { forgotPasswordMailer } from "../../../mailers/forgot-password-mailer";
|
||||
import { ForgotPassword } from "../validations";
|
||||
|
||||
const RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS = 4;
|
||||
const RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS = 24;
|
||||
|
||||
export default resolver.pipe(resolver.zod(ForgotPassword), async ({ email }) => {
|
||||
const user = await db.user.findFirst({ where: { email: email.toLowerCase() } });
|
||||
@ -36,5 +36,11 @@ async function updatePassword(user: User | null) {
|
||||
sentTo: user.email,
|
||||
},
|
||||
});
|
||||
await forgotPasswordMailer({ to: user.email, token }).send();
|
||||
await (
|
||||
await forgotPasswordMailer({
|
||||
to: user.email,
|
||||
token,
|
||||
userName: user.fullName,
|
||||
})
|
||||
).send();
|
||||
}
|
||||
|
@ -1,26 +1,27 @@
|
||||
import type { BlitzPage } from "blitz";
|
||||
import { Routes, useMutation } from "blitz";
|
||||
|
||||
import BaseLayout from "../../core/layouts/base-layout";
|
||||
import BaseLayout from "app/core/layouts/base-layout";
|
||||
import { AuthForm as Form, FORM_ERROR } from "../components/auth-form";
|
||||
import { LabeledTextField } from "../components/labeled-text-field";
|
||||
import { ForgotPassword } from "../validations";
|
||||
import forgotPassword from "../../auth/mutations/forgot-password";
|
||||
import forgotPassword from "app/auth/mutations/forgot-password";
|
||||
|
||||
const ForgotPasswordPage: BlitzPage = () => {
|
||||
const [forgotPasswordMutation, { isSuccess }] = useMutation(forgotPassword);
|
||||
const [forgotPasswordMutation, { isSuccess, reset }] = useMutation(forgotPassword);
|
||||
|
||||
return (
|
||||
<Form
|
||||
texts={{
|
||||
title: isSuccess ? "Request submitted" : "Forgot your password?",
|
||||
subtitle: "",
|
||||
submit: isSuccess ? "" : "Send reset password instructions",
|
||||
submit: isSuccess ? "" : "Send reset password link",
|
||||
}}
|
||||
schema={ForgotPassword}
|
||||
initialValues={{ email: "" }}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
reset();
|
||||
await forgotPasswordMutation(values);
|
||||
} catch (error: any) {
|
||||
return {
|
||||
@ -30,7 +31,9 @@ const ForgotPasswordPage: BlitzPage = () => {
|
||||
}}
|
||||
>
|
||||
{isSuccess ? (
|
||||
<p>If your email is in our system, you will receive instructions to reset your password shortly.</p>
|
||||
<p className="text-center">
|
||||
If your email is in our system, you will receive instructions to reset your password shortly.
|
||||
</p>
|
||||
) : (
|
||||
<LabeledTextField name="email" label="Email" />
|
||||
)}
|
||||
@ -40,6 +43,6 @@ const ForgotPasswordPage: BlitzPage = () => {
|
||||
|
||||
ForgotPasswordPage.redirectAuthenticatedTo = Routes.Messages();
|
||||
|
||||
ForgotPasswordPage.getLayout = (page) => <BaseLayout title="Forgot Your Password?">{page}</BaseLayout>;
|
||||
ForgotPasswordPage.getLayout = (page) => <BaseLayout title="Reset password">{page}</BaseLayout>;
|
||||
|
||||
export default ForgotPasswordPage;
|
||||
|
@ -56,7 +56,7 @@ const ResetPasswordPage: BlitzPage = () => {
|
||||
|
||||
ResetPasswordPage.redirectAuthenticatedTo = Routes.Messages();
|
||||
|
||||
ResetPasswordPage.getLayout = (page) => <BaseLayout title="Reset Your Password">{page}</BaseLayout>;
|
||||
ResetPasswordPage.getLayout = (page) => <BaseLayout title="Reset password">{page}</BaseLayout>;
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
if (!context.query.token) {
|
||||
|
@ -55,6 +55,6 @@ const SignIn: BlitzPage = () => {
|
||||
|
||||
SignIn.redirectAuthenticatedTo = Routes.Messages();
|
||||
|
||||
SignIn.getLayout = (page) => <BaseLayout title="Sign In">{page}</BaseLayout>;
|
||||
SignIn.getLayout = (page) => <BaseLayout title="Sign in">{page}</BaseLayout>;
|
||||
|
||||
export default SignIn;
|
||||
|
@ -55,6 +55,6 @@ SignUp.redirectAuthenticatedTo = ({ session }) => {
|
||||
return Routes.Messages();
|
||||
};
|
||||
|
||||
SignUp.getLayout = (page) => <BaseLayout title="Sign Up">{page}</BaseLayout>;
|
||||
SignUp.getLayout = (page) => <BaseLayout title="Sign up">{page}</BaseLayout>;
|
||||
|
||||
export default SignUp;
|
||||
|
@ -10,7 +10,7 @@ const BaseLayout = ({ title, children }: LayoutProps) => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{title || "shellphone.app"}</title>
|
||||
<title>{title ? `${title} | Shellphone` : "Shellphone"}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
|
@ -15,12 +15,14 @@ const notifyEmailChangeQueue = Queue<Payload>("api/queue/notify-email-change", a
|
||||
sendEmail({
|
||||
recipients: [oldEmail],
|
||||
subject: "",
|
||||
body: "",
|
||||
text: "",
|
||||
html: "",
|
||||
}),
|
||||
sendEmail({
|
||||
recipients: [newEmail],
|
||||
subject: "",
|
||||
body: "",
|
||||
text: "",
|
||||
html: "",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
@ -78,7 +78,8 @@ export const subscriptionCreatedQueue = Queue<Payload>("api/queue/subscription-c
|
||||
if (isReturningSubscriber) {
|
||||
sendEmail({
|
||||
subject: "Welcome back to Shellphone",
|
||||
body: "Welcome back to Shellphone",
|
||||
text: "Welcome back to Shellphone",
|
||||
html: "Welcome back to Shellphone",
|
||||
recipients: [email],
|
||||
}).catch((error) => {
|
||||
logger.error(error);
|
||||
@ -89,7 +90,8 @@ export const subscriptionCreatedQueue = Queue<Payload>("api/queue/subscription-c
|
||||
|
||||
sendEmail({
|
||||
subject: "Welcome to Shellphone",
|
||||
body: `Welcome to Shellphone`,
|
||||
text: `Welcome to Shellphone`,
|
||||
html: `Welcome to Shellphone`,
|
||||
recipients: [email],
|
||||
}).catch((error) => {
|
||||
logger.error(error);
|
||||
|
@ -61,7 +61,8 @@ export const subscriptionUpdatedQueue = Queue<Payload>("api/queue/subscription-u
|
||||
|
||||
sendEmail({
|
||||
subject: "Thanks for your purchase",
|
||||
body: "Thanks for your purchase",
|
||||
text: "Thanks for your purchase",
|
||||
html: "Thanks for your purchase",
|
||||
recipients: [email],
|
||||
}).catch((error) => {
|
||||
logger.error(error);
|
||||
|
@ -31,7 +31,8 @@ export default async function backup(schedule: "daily" | "weekly" | "monthly") {
|
||||
gzipChild.on("exit", (code) => {
|
||||
if (code !== 0) {
|
||||
return sendEmail({
|
||||
body: `${schedule} backup failed: gzip: Bad exit code (${code})`,
|
||||
text: `${schedule} backup failed: gzip: Bad exit code (${code})`,
|
||||
html: `${schedule} backup failed: gzip: Bad exit code (${code})`,
|
||||
subject: `${schedule} backup failed: gzip: Bad exit code (${code})`,
|
||||
recipients: ["error@shellphone.app"],
|
||||
});
|
||||
@ -44,7 +45,8 @@ export default async function backup(schedule: "daily" | "weekly" | "monthly") {
|
||||
if (code !== 0) {
|
||||
console.log("pg_dump failed, upload aborted");
|
||||
return sendEmail({
|
||||
body: `${schedule} backup failed: pg_dump: Bad exit code (${code})`,
|
||||
text: `${schedule} backup failed: pg_dump: Bad exit code (${code})`,
|
||||
html: `${schedule} backup failed: pg_dump: Bad exit code (${code})`,
|
||||
subject: `${schedule} backup failed: pg_dump: Bad exit code (${code})`,
|
||||
recipients: ["error@shellphone.app"],
|
||||
});
|
||||
@ -67,7 +69,8 @@ export default async function backup(schedule: "daily" | "weekly" | "monthly") {
|
||||
.catch((error) => {
|
||||
logger.error(error);
|
||||
return sendEmail({
|
||||
body: `${schedule} backup failed: ${error}`,
|
||||
text: `${schedule} backup failed: ${error}`,
|
||||
html: `${schedule} backup failed: ${error}`,
|
||||
subject: `${schedule} backup failed: ${error}`,
|
||||
recipients: ["error@shellphone.app"],
|
||||
});
|
||||
|
@ -11,19 +11,24 @@ const credentials = new Credentials({
|
||||
const ses = new SES({ region: serverRuntimeConfig.awsSes.awsRegion, credentials });
|
||||
|
||||
type SendEmailParams = {
|
||||
body: string;
|
||||
text: string;
|
||||
html: string;
|
||||
subject: string;
|
||||
recipients: string[];
|
||||
};
|
||||
|
||||
export async function sendEmail({ body, subject, recipients }: SendEmailParams) {
|
||||
export async function sendEmail({ text, html, subject, recipients }: SendEmailParams) {
|
||||
const request: SendEmailRequest = {
|
||||
Destination: { ToAddresses: recipients },
|
||||
Message: {
|
||||
Body: {
|
||||
Text: {
|
||||
Charset: "UTF-8",
|
||||
Data: body,
|
||||
Data: text,
|
||||
},
|
||||
Html: {
|
||||
Charset: "UTF-8",
|
||||
Data: html,
|
||||
},
|
||||
},
|
||||
Subject: {
|
||||
|
16
mailers/components/footer.html
Normal file
16
mailers/components/footer.html
Normal file
@ -0,0 +1,16 @@
|
||||
<tr>
|
||||
<td>
|
||||
<table align="center" class="email-footer w-570 mx-auto text-center sm:w-full">
|
||||
<tr>
|
||||
<td align="center" class="content-cell p-45 text-base">
|
||||
<p class="mt-6 mb-20 text-xs leading-24 text-center text-gray-postmark-light">
|
||||
© {{ page.year }} {{ page.company.product }}. All rights reserved.
|
||||
</p>
|
||||
<p class="mt-6 mb-20 text-xs leading-24 text-center text-gray-postmark-light">
|
||||
{{ page.company.name }} {{{ page.company.address }}}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
11
mailers/components/header.html
Normal file
11
mailers/components/header.html
Normal file
@ -0,0 +1,11 @@
|
||||
<tr>
|
||||
<td align="center" class="email-masthead">
|
||||
<a
|
||||
href="https://www.shellphone.app"
|
||||
class="email-masthead_name text-base font-bold no-underline text-gray-postmark-light"
|
||||
style="text-shadow: 0 1px 0 #ffffff"
|
||||
>
|
||||
<img width="64px" src="https://www.shellphone.app/shellphone.png" alt="Shellphone logo" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
32
mailers/custom/postmark/buttons.css
Normal file
32
mailers/custom/postmark/buttons.css
Normal file
@ -0,0 +1,32 @@
|
||||
.button {
|
||||
@apply inline-block text-white no-underline;
|
||||
background-color: #3869d4;
|
||||
border-top: 10px solid #3869d4;
|
||||
border-right: 18px solid #3869d4;
|
||||
border-bottom: 10px solid #3869d4;
|
||||
border-left: 18px solid #3869d4;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
|
||||
.button--green {
|
||||
background-color: #22bc66;
|
||||
border-top: 10px solid #22bc66;
|
||||
border-right: 18px solid #22bc66;
|
||||
border-bottom: 10px solid #22bc66;
|
||||
border-left: 18px solid #22bc66;
|
||||
}
|
||||
|
||||
.button--red {
|
||||
background-color: #ff6136;
|
||||
border-top: 10px solid #ff6136;
|
||||
border-right: 18px solid #ff6136;
|
||||
border-bottom: 10px solid #ff6136;
|
||||
border-left: 18px solid #ff6136;
|
||||
}
|
||||
|
||||
@screen sm {
|
||||
.button {
|
||||
@apply w-full text-center !important;
|
||||
}
|
||||
}
|
65
mailers/custom/postmark/index.css
Normal file
65
mailers/custom/postmark/index.css
Normal file
@ -0,0 +1,65 @@
|
||||
@import "buttons";
|
||||
|
||||
.purchase_heading {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: #eaeaec;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
.purchase_heading p {
|
||||
@apply text-xxs leading-24 m-0;
|
||||
color: #85878e;
|
||||
}
|
||||
|
||||
.purchase_footer {
|
||||
@apply pt-16 text-base align-middle;
|
||||
border-top-width: 1px;
|
||||
border-top-color: #eaeaec;
|
||||
border-top-style: solid;
|
||||
}
|
||||
|
||||
.body-sub {
|
||||
@apply mt-25 pt-25 border-t;
|
||||
border-top-color: #eaeaec;
|
||||
border-top-style: solid;
|
||||
}
|
||||
|
||||
.discount {
|
||||
@apply w-full p-24 bg-gray-postmark-lightest;
|
||||
border: 2px dashed #cbcccf;
|
||||
}
|
||||
|
||||
.email-masthead {
|
||||
@apply py-24 text-base text-center;
|
||||
}
|
||||
|
||||
@screen dark {
|
||||
body,
|
||||
.email-body,
|
||||
.email-body_inner,
|
||||
.email-content,
|
||||
.email-wrapper,
|
||||
.email-masthead,
|
||||
.email-footer {
|
||||
@apply bg-gray-postmark-darker text-white !important;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
blockquote,
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
@apply text-white !important;
|
||||
}
|
||||
|
||||
.attributes_content,
|
||||
.discount {
|
||||
@apply bg-gray-postmark-darkest !important;
|
||||
}
|
||||
|
||||
.email-masthead_name {
|
||||
text-shadow: none !important;
|
||||
}
|
||||
}
|
10
mailers/custom/reset.css
Normal file
10
mailers/custom/reset.css
Normal file
@ -0,0 +1,10 @@
|
||||
body {
|
||||
@apply m-0 p-0 w-full;
|
||||
word-break: break-word;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
@apply max-w-full leading-full align-middle;
|
||||
}
|
3
mailers/custom/utilities.css
Normal file
3
mailers/custom/utilities.css
Normal file
@ -0,0 +1,3 @@
|
||||
.mso-leading-exactly {
|
||||
mso-line-height-rule: exactly;
|
||||
}
|
@ -1,41 +1,39 @@
|
||||
/* TODO - You need to add a mailer integration in `integrations/` and import here.
|
||||
*
|
||||
* The integration file can be very simple. Instantiate the email client
|
||||
* and then export it. That way you can import here and anywhere else
|
||||
* and use it straight away.
|
||||
*/
|
||||
import previewEmail from "preview-email";
|
||||
|
||||
import { sendEmail } from "integrations/aws-ses";
|
||||
import { render, plaintext } from "./renderer";
|
||||
|
||||
type ResetPasswordMailer = {
|
||||
to: string;
|
||||
token: string;
|
||||
userName: string;
|
||||
};
|
||||
|
||||
export function forgotPasswordMailer({ to, token }: ResetPasswordMailer) {
|
||||
export async function forgotPasswordMailer({ to, token, userName }: ResetPasswordMailer) {
|
||||
// In production, set APP_ORIGIN to your production server origin
|
||||
const origin = process.env.APP_ORIGIN || process.env.BLITZ_DEV_SERVER_ORIGIN;
|
||||
const resetUrl = `${origin}/reset-password?token=${token}`;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
render("forgot-password", { action_url: resetUrl, name: userName }),
|
||||
plaintext("forgot-password", { action_url: resetUrl, name: userName }),
|
||||
]);
|
||||
const msg = {
|
||||
from: "TODO@example.com",
|
||||
from: "mokhtar@shellphone.app",
|
||||
to,
|
||||
subject: "Your Password Reset Instructions",
|
||||
html: `
|
||||
<h1>Reset Your Password</h1>
|
||||
<h3>NOTE: You must set up a production email integration in mailers/forgotPasswordMailer.ts</h3>
|
||||
|
||||
<a href="${resetUrl}">
|
||||
Click here to set a new password
|
||||
</a>
|
||||
`,
|
||||
subject: "Reset your password",
|
||||
html,
|
||||
text,
|
||||
};
|
||||
|
||||
return {
|
||||
async send() {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
// TODO - send the production email, like this:
|
||||
// await postmark.sendEmail(msg)
|
||||
throw new Error("No production email implementation in mailers/forgotPasswordMailer");
|
||||
await sendEmail({
|
||||
recipients: [msg.to],
|
||||
subject: msg.subject,
|
||||
html: msg.html,
|
||||
text: msg.text,
|
||||
});
|
||||
} else {
|
||||
// Preview email in the browser
|
||||
await previewEmail(msg);
|
||||
|
75
mailers/layouts/main.html
Normal file
75
mailers/layouts/main.html
Normal file
@ -0,0 +1,75 @@
|
||||
<!DOCTYPE {{{ page.doctype || 'html' }}}>
|
||||
<html lang="{{ page.language || 'en' }}" xmlns:v="urn:schemas-microsoft-com:vml">
|
||||
<head>
|
||||
<meta charset="{{ page.charset || 'utf-8' }}" />
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no" />
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<style>
|
||||
td,
|
||||
th,
|
||||
div,
|
||||
p,
|
||||
a,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
mso-line-height-rule: exactly;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<if condition="page.title">
|
||||
<title>{{{ page.title }}}</title>
|
||||
</if>
|
||||
<if condition="page.googleFonts">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?{{{ page.googleFonts }}}&display=swap"
|
||||
rel="stylesheet"
|
||||
media="screen"
|
||||
/>
|
||||
</if>
|
||||
<if condition="page.css">
|
||||
<style>
|
||||
{{{ page.css }}}
|
||||
</style>
|
||||
</if>
|
||||
<block name="head"></block>
|
||||
</head>
|
||||
<body class="{{ page.bodyClass }}">
|
||||
<if condition="page.preheader">
|
||||
<div class="hidden">
|
||||
{{{ page.preheader }}}͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
‌  ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏
|
||||
͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ͏ ‌
|
||||
 ͏ ͏ ͏ ͏ ͏
|
||||
</div>
|
||||
</if>
|
||||
<div
|
||||
role="article"
|
||||
aria-roledescription="email"
|
||||
aria-label="{{{ page.title || '' }}}"
|
||||
lang="{{ page.language || 'en' }}"
|
||||
>
|
||||
<block name="template"></block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
225
mailers/renderer.ts
Normal file
225
mailers/renderer.ts
Normal file
@ -0,0 +1,225 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
// @ts-ignore
|
||||
import Maizzle from "@maizzle/framework";
|
||||
|
||||
export async function render(templateName: string, locals: Record<string, string> = {}) {
|
||||
const { template, options } = getMaizzleParams(templateName, locals);
|
||||
const { html } = await Maizzle.render(template, options);
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
export async function plaintext(templateName: string, locals: Record<string, string> = {}) {
|
||||
const { template, options } = getMaizzleParams(templateName, locals);
|
||||
const { plaintext } = await Maizzle.plaintext(template, options);
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
function getMaizzleParams(templateName: string, locals: Record<string, string>) {
|
||||
const template = fs
|
||||
.readFileSync(path.resolve(process.cwd(), "./mailers/templates", `${templateName}.html`))
|
||||
.toString();
|
||||
const tailwindCss = fs.readFileSync(path.resolve(process.cwd(), "./mailers/tailwind.css")).toString();
|
||||
|
||||
const options = {
|
||||
tailwind: {
|
||||
css: tailwindCss,
|
||||
config: {
|
||||
mode: "jit",
|
||||
theme: {
|
||||
screens: {
|
||||
sm: { max: "600px" },
|
||||
dark: { raw: "(prefers-color-scheme: dark)" },
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
gray: {
|
||||
"postmark-lightest": "#F4F4F7",
|
||||
"postmark-lighter": "#F2F4F6",
|
||||
"postmark-light": "#A8AAAF",
|
||||
"postmark-dark": "#51545E",
|
||||
"postmark-darker": "#333333",
|
||||
"postmark-darkest": "#222222",
|
||||
"postmark-meta": "#85878E",
|
||||
},
|
||||
blue: {
|
||||
postmark: "#3869D4",
|
||||
},
|
||||
},
|
||||
spacing: {
|
||||
screen: "100vw",
|
||||
full: "100%",
|
||||
px: "1px",
|
||||
0: "0",
|
||||
2: "2px",
|
||||
3: "3px",
|
||||
4: "4px",
|
||||
5: "5px",
|
||||
6: "6px",
|
||||
7: "7px",
|
||||
8: "8px",
|
||||
9: "9px",
|
||||
10: "10px",
|
||||
11: "11px",
|
||||
12: "12px",
|
||||
14: "14px",
|
||||
16: "16px",
|
||||
20: "20px",
|
||||
21: "21px",
|
||||
24: "24px",
|
||||
25: "25px",
|
||||
28: "28px",
|
||||
30: "30px",
|
||||
32: "32px",
|
||||
35: "35px",
|
||||
36: "36px",
|
||||
40: "40px",
|
||||
44: "44px",
|
||||
45: "45px",
|
||||
48: "48px",
|
||||
52: "52px",
|
||||
56: "56px",
|
||||
60: "60px",
|
||||
64: "64px",
|
||||
72: "72px",
|
||||
80: "80px",
|
||||
96: "96px",
|
||||
570: "570px",
|
||||
600: "600px",
|
||||
"1/2": "50%",
|
||||
"1/3": "33.333333%",
|
||||
"2/3": "66.666667%",
|
||||
"1/4": "25%",
|
||||
"2/4": "50%",
|
||||
"3/4": "75%",
|
||||
"1/5": "20%",
|
||||
"2/5": "40%",
|
||||
"3/5": "60%",
|
||||
"4/5": "80%",
|
||||
"1/6": "16.666667%",
|
||||
"2/6": "33.333333%",
|
||||
"3/6": "50%",
|
||||
"4/6": "66.666667%",
|
||||
"5/6": "83.333333%",
|
||||
"1/12": "8.333333%",
|
||||
"2/12": "16.666667%",
|
||||
"3/12": "25%",
|
||||
"4/12": "33.333333%",
|
||||
"5/12": "41.666667%",
|
||||
"6/12": "50%",
|
||||
"7/12": "58.333333%",
|
||||
"8/12": "66.666667%",
|
||||
"9/12": "75%",
|
||||
"10/12": "83.333333%",
|
||||
"11/12": "91.666667%",
|
||||
},
|
||||
borderRadius: {
|
||||
none: "0px",
|
||||
sm: "2px",
|
||||
DEFAULT: "4px",
|
||||
md: "6px",
|
||||
lg: "8px",
|
||||
xl: "12px",
|
||||
"2xl": "16px",
|
||||
"3xl": "24px",
|
||||
full: "9999px",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['"Nunito Sans"', "-apple-system", '"Segoe UI"', "sans-serif"],
|
||||
serif: ["Constantia", "Georgia", "serif"],
|
||||
mono: ["Menlo", "Consolas", "monospace"],
|
||||
},
|
||||
fontSize: {
|
||||
0: "0",
|
||||
xxs: "12px",
|
||||
xs: "13px",
|
||||
sm: "14px",
|
||||
base: "16px",
|
||||
lg: "18px",
|
||||
xl: "20px",
|
||||
"2xl": "24px",
|
||||
"3xl": "30px",
|
||||
"4xl": "36px",
|
||||
"5xl": "48px",
|
||||
"6xl": "60px",
|
||||
"7xl": "72px",
|
||||
"8xl": "96px",
|
||||
"9xl": "128px",
|
||||
},
|
||||
inset: (theme: TailwindThemeHelper) => ({
|
||||
...theme("spacing"),
|
||||
}),
|
||||
letterSpacing: (theme: TailwindThemeHelper) => ({
|
||||
...theme("spacing"),
|
||||
}),
|
||||
lineHeight: (theme: TailwindThemeHelper) => ({
|
||||
...theme("spacing"),
|
||||
}),
|
||||
maxHeight: (theme: TailwindThemeHelper) => ({
|
||||
...theme("spacing"),
|
||||
}),
|
||||
maxWidth: (theme: TailwindThemeHelper) => ({
|
||||
...theme("spacing"),
|
||||
xs: "160px",
|
||||
sm: "192px",
|
||||
md: "224px",
|
||||
lg: "256px",
|
||||
xl: "288px",
|
||||
"2xl": "336px",
|
||||
"3xl": "384px",
|
||||
"4xl": "448px",
|
||||
"5xl": "512px",
|
||||
"6xl": "576px",
|
||||
"7xl": "640px",
|
||||
}),
|
||||
minHeight: (theme: TailwindThemeHelper) => ({
|
||||
...theme("spacing"),
|
||||
}),
|
||||
minWidth: (theme: TailwindThemeHelper) => ({
|
||||
...theme("spacing"),
|
||||
}),
|
||||
},
|
||||
},
|
||||
corePlugins: {
|
||||
animation: false,
|
||||
backgroundOpacity: false,
|
||||
borderOpacity: false,
|
||||
divideOpacity: false,
|
||||
placeholderOpacity: false,
|
||||
textOpacity: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
maizzle: {
|
||||
build: {
|
||||
posthtml: {
|
||||
expressions: {
|
||||
locals,
|
||||
},
|
||||
},
|
||||
},
|
||||
company: {
|
||||
name: "Capsule Corp. Dev Pte. Ltd.",
|
||||
address: `
|
||||
<br>39 Robinson Rd, #11-01
|
||||
<br>Singapore 068911
|
||||
`,
|
||||
product: "Shellphone",
|
||||
},
|
||||
googleFonts: "family=Nunito+Sans:wght@400;700",
|
||||
year: () => new Date().getFullYear(),
|
||||
inlineCSS: true,
|
||||
prettify: true,
|
||||
removeUnusedCSS: true,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
template,
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
type TailwindThemeHelper = (str: string) => {};
|
18
mailers/tailwind.css
Normal file
18
mailers/tailwind.css
Normal file
@ -0,0 +1,18 @@
|
||||
/* Your custom CSS resets for email */
|
||||
@import "mailers/custom/reset";
|
||||
|
||||
/* Tailwind components that are generated by plugins */
|
||||
@import "tailwindcss/components";
|
||||
|
||||
/**
|
||||
* @import here any custom components - classes that you'd want loaded
|
||||
* before the Tailwind utilities, so that the utilities could still
|
||||
* override them.
|
||||
*/
|
||||
@import "mailers/custom/postmark";
|
||||
|
||||
/* Tailwind utility classes */
|
||||
@import "tailwindcss/utilities";
|
||||
|
||||
/* Your custom utility classes */
|
||||
@import "mailers/custom/utilities";
|
92
mailers/templates/forgot-password.html
Normal file
92
mailers/templates/forgot-password.html
Normal file
@ -0,0 +1,92 @@
|
||||
---
|
||||
bodyClass: bg-gray-postmark-lighter
|
||||
---
|
||||
|
||||
<extends src="mailers/layouts/main.html">
|
||||
<block name="template">
|
||||
<table class="email-wrapper w-full bg-gray-postmark-lighter font-sans">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table class="email-content w-full">
|
||||
<component src="mailers/components/header.html"></component>
|
||||
<tr>
|
||||
<td class="email-body w-full">
|
||||
<table align="center" class="email-body_inner w-570 bg-white mx-auto sm:w-full">
|
||||
<tr>
|
||||
<td class="px-45 py-24">
|
||||
<div class="text-base">
|
||||
<h1 class="mt-0 text-2xl font-bold text-left text-gray-postmark-darker">
|
||||
Hi {{name}},
|
||||
</h1>
|
||||
<p class="mt-6 mb-20 text-base leading-24 text-gray-postmark-dark">
|
||||
You recently requested to reset your password for your
|
||||
{{page.company.product}} account. Use the button below to reset it.
|
||||
<strong
|
||||
>This password reset is only valid for the next 24
|
||||
hours.</strong
|
||||
>
|
||||
</p>
|
||||
<table align="center" class="w-full text-center my-30 mx-auto">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table class="w-full">
|
||||
<tr>
|
||||
<td align="center" class="text-base">
|
||||
<a
|
||||
href="{{action_url}}"
|
||||
class="button button--green"
|
||||
target="_blank"
|
||||
>Reset your password</a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p class="mt-6 mb-20 text-base leading-24 text-gray-postmark-dark">
|
||||
If you did not request a password reset, you can safely ignore this
|
||||
email.
|
||||
</p>
|
||||
<table class="body-sub">
|
||||
<tr>
|
||||
<td>
|
||||
<p
|
||||
class="
|
||||
mt-6
|
||||
mb-20
|
||||
text-xs
|
||||
leading-24
|
||||
text-gray-postmark-dark
|
||||
"
|
||||
>
|
||||
If you're having trouble with the button above, copy and
|
||||
paste the URL below into your web browser.
|
||||
</p>
|
||||
<p
|
||||
class="
|
||||
mt-6
|
||||
mb-20
|
||||
text-xs
|
||||
leading-24
|
||||
text-gray-postmark-dark
|
||||
"
|
||||
>
|
||||
{{action_url}}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!--<component src="mailers/components/footer.html"></component>-->
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</block>
|
||||
</extends>
|
5919
package-lock.json
generated
5919
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -36,6 +36,7 @@
|
||||
"@devoxa/paddle-sdk": "0.2.1",
|
||||
"@headlessui/react": "1.4.1",
|
||||
"@hookform/resolvers": "2.8.2",
|
||||
"@maizzle/framework": "3.7.2",
|
||||
"@panelbear/panelbear-js": "1.3.2",
|
||||
"@prisma/client": "3.2.1",
|
||||
"@react-aria/interactions": "3.6.0",
|
||||
|
Loading…
Reference in New Issue
Block a user