chore(examples): migrates email example to 3.0 [skip-lint] (#9215)
Changes: - Migrates `email` example project to `3.0` from `2.0` - Replaces `inline-css` dependency with `juice` package instead. - Replaces `Handlebars` dependency with `ejs` package instead. Reason for replacing packages: - Both `inline-css` & `Handlebars` had issues with Nextjs and its Webpack bundling i.e does not support `require.extensions`. - `ejs` & `juice` do not rely on `require.extensions`.
This commit is contained in:
1
.github/workflows/main.yml
vendored
1
.github/workflows/main.yml
vendored
@@ -49,6 +49,7 @@ jobs:
|
|||||||
- 'pnpm-lock.yaml'
|
- 'pnpm-lock.yaml'
|
||||||
- 'package.json'
|
- 'package.json'
|
||||||
- 'templates/**'
|
- 'templates/**'
|
||||||
|
- 'examples/**'
|
||||||
templates:
|
templates:
|
||||||
- 'templates/**'
|
- 'templates/**'
|
||||||
- name: Log all filter results
|
- name: Log all filter results
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
MONGODB_URI=mongodb://127.0.0.1/payload-example-email
|
DATABASE_URI=mongodb://127.0.0.1/payload-example-email
|
||||||
PAYLOAD_SECRET=
|
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:8000
|
PAYLOAD_SECRET=PAYLOAD_EMAIL_EXAMPLE_SECRET_KEY
|
||||||
|
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
extends: ['@payloadcms'],
|
extends: ['@payloadcms'],
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/no-unused-vars': 'warn',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
24
examples/email/.swcrc
Normal file
24
examples/email/.swcrc
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/swcrc",
|
||||||
|
"sourceMaps": true,
|
||||||
|
"jsc": {
|
||||||
|
"target": "esnext",
|
||||||
|
"parser": {
|
||||||
|
"syntax": "typescript",
|
||||||
|
"tsx": true,
|
||||||
|
"dts": true
|
||||||
|
},
|
||||||
|
"transform": {
|
||||||
|
"react": {
|
||||||
|
"runtime": "automatic",
|
||||||
|
"pragmaFrag": "React.Fragment",
|
||||||
|
"throwIfNamespace": true,
|
||||||
|
"development": false,
|
||||||
|
"useBuiltins": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"module": {
|
||||||
|
"type": "es6"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,30 +7,31 @@ This example demonstrates how to integrate email functionality into Payload.
|
|||||||
To spin up this example locally, follow these steps:
|
To spin up this example locally, follow these steps:
|
||||||
|
|
||||||
1. Clone this repo
|
1. Clone this repo
|
||||||
2. `cd` into this directory and run `yarn` or `npm install`
|
2. `cp .env.example .env` to copy the example environment variables
|
||||||
3. `cp .env.example .env` to copy the example environment variables
|
3. `pnpm install && pnpm dev` to install dependencies and start the dev server
|
||||||
4. `yarn dev` or `npm run dev` to start the server and seed the database
|
4. `yarn dev` or `npm run dev` to start the server and seed the database
|
||||||
5. `open http://localhost:8000/admin` to access the admin panel
|
5. open `http://localhost:3000/admin` to access the admin panel
|
||||||
6. Create your first user
|
6. Create your first user
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
Payload utilizes [NodeMailer](https://nodemailer.com/about/) for email functionality. Once you add your email configuration to `payload.init()`, you send email from anywhere in your application just by calling `payload.sendEmail({})`.
|
Email functionality in Payload is configured using adapters. The recommended adapter for most use cases is the [@payloadcms/email-nodemailer](https://www.npmjs.com/package/@payloadcms/email-nodemailer) package.
|
||||||
|
|
||||||
1. Navigate to `src/server.ts` - this is where your email config gets passed to Payload
|
To enable email, pass your adapter configuration to the `email` property in the Payload Config. This allows Payload to send auth-related emails for password resets, new user verifications, and other email needs.
|
||||||
2. Open `src/email/transport.ts` - here we are defining the email config. You can use an env variable to switch between the mock email transport and live email service.
|
|
||||||
|
1. In the Payload Config file, add your email adapter to the `email` property. For example, the `@payloadcms/email-nodemailer` adapter can be configured for SMTP, SendGrid, or other supported transports. During development, if no configuration is provided, Payload will use a mock service via [ethereal.email](ethereal.email).
|
||||||
|
|
||||||
Now we can start sending email!
|
Now we can start sending email!
|
||||||
|
|
||||||
3. Go to `src/collections/Newsletter.ts` - with an `afterChange` hook, we are sending an email when a new user signs up for the newsletter
|
2. Go to `src/collections/Newsletter.ts` - with an `afterChange` hook, we are sending an email when a new user signs up for the newsletter
|
||||||
|
|
||||||
Let's not forget our authentication emails...
|
Let's not forget our authentication emails...
|
||||||
|
|
||||||
4. Auth-enabled collections have built-in options to verify the user and reset the user password. Open `src/collections/Users.ts` and see how we customize these emails.
|
3. Auth-enabled collections have built-in options to verify the user and reset the user password. Open `src/collections/Users.ts` and see how we customize these emails.
|
||||||
|
|
||||||
Speaking of customization...
|
Speaking of customization...
|
||||||
|
|
||||||
5. Take a look at `src/email/generateEmailHTML` and how it compiles a custom template when sending email. You change this to any HTML template of your choosing.
|
4. Take a look at `src/email/generateEmailHTML` and how it compiles a custom template when sending email. You change this to any HTML template of your choosing.
|
||||||
|
|
||||||
That's all you need, now you can go ahead and test out this repo by creating a new `user` or `newsletter-signup` and see the email integration in action.
|
That's all you need, now you can go ahead and test out this repo by creating a new `user` or `newsletter-signup` and see the email integration in action.
|
||||||
|
|
||||||
@@ -40,10 +41,10 @@ To spin up this example locally, follow the [Quick Start](#quick-start).
|
|||||||
|
|
||||||
## Production
|
## Production
|
||||||
|
|
||||||
To run Payload in production, you need to build and serve the Admin panel. To do so, follow these steps:
|
To run Payload in production, you need to build and start the Admin panel. To do so, follow these steps:
|
||||||
|
|
||||||
1. First invoke the `payload build` script by running `yarn build` or `npm run build` in your project root. This creates a `./build` directory with a production-ready admin bundle.
|
1. Invoke the `next build` script by running `pnpm build` or `npm run build` in your project root. This creates a `.next` directory with a production-ready admin bundle.
|
||||||
1. Then run `yarn serve` or `npm run serve` to run Node in production and serve Payload from the `./build` directory.
|
1. Finally run `pnpm start` or `npm run start` to run Node in production and serve Payload from the `.build` directory.
|
||||||
|
|
||||||
### Deployment
|
### Deployment
|
||||||
|
|
||||||
|
|||||||
5
examples/email/next-env.d.ts
vendored
Normal file
5
examples/email/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||||
8
examples/email/next.config.mjs
Normal file
8
examples/email/next.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { withPayload } from '@payloadcms/next/withPayload'
|
||||||
|
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
// Your Next.js config here
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withPayload(nextConfig)
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"ext": "ts",
|
|
||||||
"exec": "ts-node src/server.ts -- -I",
|
|
||||||
"stdin": false
|
|
||||||
}
|
|
||||||
@@ -1,35 +1,57 @@
|
|||||||
{
|
{
|
||||||
"name": "payload-example-email",
|
"name": "payload-example-email",
|
||||||
"description": "Payload Email integration example.",
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "dist/server.js",
|
"description": "Payload Email integration example.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cross-env PAYLOAD_SEED=true PAYLOAD_DROP_DATABASE=true PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
|
"_dev": "cross-env NODE_OPTIONS=--no-deprecation next dev",
|
||||||
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
|
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
|
||||||
"build:server": "tsc",
|
"dev": "cross-env PAYLOAD_SEED=true PAYLOAD_DROP_DATABASE=true NODE_OPTIONS=--no-deprecation next dev",
|
||||||
"build": "yarn copyfiles && yarn build:payload && yarn build:server",
|
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
|
||||||
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
|
"generate:schema": "payload-graphql generate:schema",
|
||||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
|
"generate:types": "payload generate:types",
|
||||||
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
|
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
||||||
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
|
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
|
||||||
"lint": "eslint src",
|
|
||||||
"lint:fix": "eslint --fix --ext .ts,.tsx src"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@payloadcms/db-mongodb": "beta",
|
||||||
|
"@payloadcms/email-nodemailer": "beta",
|
||||||
|
"@payloadcms/next": "beta",
|
||||||
|
"@payloadcms/richtext-lexical": "beta",
|
||||||
|
"@payloadcms/ui": "beta",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"express": "^4.17.1",
|
"ejs": "3.1.10",
|
||||||
"payload": "latest",
|
"graphql": "^16.9.0",
|
||||||
"handlebars": "^4.7.7",
|
"juice": "11.0.0",
|
||||||
"inline-css": "^4.0.2"
|
"next": "15.0.0",
|
||||||
|
"payload": "beta",
|
||||||
|
"react": "19.0.0-rc-65a56d0e-20241020",
|
||||||
|
"react-dom": "19.0.0-rc-65a56d0e-20241020"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^4.17.9",
|
"@payloadcms/graphql": "beta",
|
||||||
"copyfiles": "^2.4.1",
|
"@swc/core": "^1.6.13",
|
||||||
"cross-env": "^7.0.3",
|
"@types/ejs": "^3.1.5",
|
||||||
"eslint": "^8.19.0",
|
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||||
"nodemon": "^2.0.6",
|
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||||
"ts-node": "^9.1.1",
|
"eslint": "^8.57.0",
|
||||||
"typescript": "^4.8.4"
|
"eslint-config-next": "15.0.0",
|
||||||
|
"tsx": "^4.16.2",
|
||||||
|
"typescript": "5.5.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.20.2 || >=20.9.0"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||||
|
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||||
|
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6932
examples/email/pnpm-lock.yaml
generated
Normal file
6932
examples/email/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
|||||||
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views'
|
||||||
|
|
||||||
|
import { importMap } from '../importMap'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
params: Promise<{
|
||||||
|
segments: string[]
|
||||||
|
}>
|
||||||
|
searchParams: Promise<{
|
||||||
|
[key: string]: string | string[]
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||||
|
generatePageMetadata({ config, params, searchParams })
|
||||||
|
|
||||||
|
const NotFound = ({ params, searchParams }: Args) =>
|
||||||
|
NotFoundPage({ config, importMap, params, searchParams })
|
||||||
|
|
||||||
|
export default NotFound
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { generatePageMetadata, RootPage } from '@payloadcms/next/views'
|
||||||
|
|
||||||
|
import { importMap } from '../importMap'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
params: Promise<{
|
||||||
|
segments: string[]
|
||||||
|
}>
|
||||||
|
searchParams: Promise<{
|
||||||
|
[key: string]: string | string[]
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||||
|
generatePageMetadata({ config, params, searchParams })
|
||||||
|
|
||||||
|
const Page = ({ params, searchParams }: Args) =>
|
||||||
|
RootPage({ config, importMap, params, searchParams })
|
||||||
|
|
||||||
|
export default Page
|
||||||
1
examples/email/src/app/(payload)/admin/importMap.js
Normal file
1
examples/email/src/app/(payload)/admin/importMap.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const importMap = {}
|
||||||
10
examples/email/src/app/(payload)/api/[...slug]/route.ts
Normal file
10
examples/email/src/app/(payload)/api/[...slug]/route.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
|
||||||
|
|
||||||
|
export const GET = REST_GET(config)
|
||||||
|
export const POST = REST_POST(config)
|
||||||
|
export const DELETE = REST_DELETE(config)
|
||||||
|
export const PATCH = REST_PATCH(config)
|
||||||
|
export const OPTIONS = REST_OPTIONS(config)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
|
||||||
|
|
||||||
|
export const GET = GRAPHQL_PLAYGROUND_GET(config)
|
||||||
8
examples/email/src/app/(payload)/api/graphql/route.ts
Normal file
8
examples/email/src/app/(payload)/api/graphql/route.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
|
||||||
|
|
||||||
|
export const POST = GRAPHQL_POST(config)
|
||||||
|
|
||||||
|
export const OPTIONS = REST_OPTIONS(config)
|
||||||
0
examples/email/src/app/(payload)/custom.scss
Normal file
0
examples/email/src/app/(payload)/custom.scss
Normal file
32
examples/email/src/app/(payload)/layout.tsx
Normal file
32
examples/email/src/app/(payload)/layout.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { ServerFunctionClient } from 'payload'
|
||||||
|
|
||||||
|
import '@payloadcms/next/css'
|
||||||
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
|
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { importMap } from './admin/importMap.js'
|
||||||
|
import './custom.scss'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverFunction: ServerFunctionClient = async function (args) {
|
||||||
|
'use server'
|
||||||
|
return handleServerFunctions({
|
||||||
|
...args,
|
||||||
|
config,
|
||||||
|
importMap,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const Layout = ({ children }: Args) => (
|
||||||
|
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
||||||
|
{children}
|
||||||
|
</RootLayout>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Layout
|
||||||
@@ -1,28 +1,12 @@
|
|||||||
import type { CollectionConfig } from 'payload/types'
|
import type { CollectionConfig } from 'payload'
|
||||||
import generateEmailHTML from '../email/generateEmailHTML'
|
|
||||||
|
|
||||||
const Newsletter: CollectionConfig = {
|
import { generateEmailHTML } from '../email/generateEmailHTML'
|
||||||
|
|
||||||
|
export const Newsletter: CollectionConfig = {
|
||||||
slug: 'newsletter-signups',
|
slug: 'newsletter-signups',
|
||||||
admin: {
|
admin: {
|
||||||
defaultColumns: ['name', 'email'],
|
defaultColumns: ['name', 'email'],
|
||||||
},
|
},
|
||||||
hooks: {
|
|
||||||
afterChange: [
|
|
||||||
async ({ doc, operation, req }) => {
|
|
||||||
if (operation === 'create') {
|
|
||||||
req.payload.sendEmail({
|
|
||||||
to: doc.email,
|
|
||||||
from: 'sender@example.com',
|
|
||||||
subject: 'Thanks for signing up!',
|
|
||||||
html: await generateEmailHTML({
|
|
||||||
headline: 'Welcome to the newsletter!',
|
|
||||||
content: `<p>${doc.name ? `Hi ${doc.name}!` : 'Hi!'} We'll be in touch soon...</p>`,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
@@ -34,6 +18,25 @@ const Newsletter: CollectionConfig = {
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
hooks: {
|
||||||
|
afterChange: [
|
||||||
|
async ({ doc, operation, req }) => {
|
||||||
|
if (operation === 'create') {
|
||||||
|
req.payload
|
||||||
|
.sendEmail({
|
||||||
|
from: 'sender@example.com',
|
||||||
|
html: await generateEmailHTML({
|
||||||
|
content: `<p>${doc.name ? `Hi ${doc.name}!` : 'Hi!'} We'll be in touch soon...</p>`,
|
||||||
|
headline: 'Welcome to the newsletter!',
|
||||||
|
}),
|
||||||
|
subject: 'Thanks for signing up!',
|
||||||
|
to: doc.email,
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error sending email:', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Newsletter
|
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import type { CollectionConfig } from 'payload/types'
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
import generateForgotPasswordEmail from '../email/generateForgotPasswordEmail'
|
import { generateForgotPasswordEmail } from '../email/generateForgotPasswordEmail'
|
||||||
import generateVerificationEmail from '../email/generateVerificationEmail'
|
import { generateVerificationEmail } from '../email/generateVerificationEmail'
|
||||||
|
|
||||||
const Users: CollectionConfig = {
|
export const Users: CollectionConfig = {
|
||||||
slug: 'users',
|
slug: 'users',
|
||||||
auth: {
|
|
||||||
verify: {
|
|
||||||
generateEmailSubject: () => 'Verify your email',
|
|
||||||
generateEmailHTML: generateVerificationEmail,
|
|
||||||
},
|
|
||||||
forgotPassword: {
|
|
||||||
generateEmailSubject: () => 'Reset your password',
|
|
||||||
generateEmailHTML: generateForgotPasswordEmail,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
admin: {
|
admin: {
|
||||||
useAsTitle: 'email',
|
useAsTitle: 'email',
|
||||||
},
|
},
|
||||||
|
auth: {
|
||||||
|
forgotPassword: {
|
||||||
|
generateEmailHTML: generateForgotPasswordEmail,
|
||||||
|
generateEmailSubject: () => 'Reset your password',
|
||||||
|
},
|
||||||
|
verify: {
|
||||||
|
generateEmailHTML: generateVerificationEmail,
|
||||||
|
generateEmailSubject: () => 'Verify your email',
|
||||||
|
},
|
||||||
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
@@ -25,5 +25,3 @@ const Users: CollectionConfig = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Users
|
|
||||||
|
|||||||
@@ -1,24 +1,17 @@
|
|||||||
|
import ejs from 'ejs'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import Handlebars from 'handlebars'
|
import juice from 'juice'
|
||||||
import inlineCSS from 'inline-css'
|
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
const template = fs.readFileSync
|
export const generateEmailHTML = async (data: any): Promise<string> => {
|
||||||
? fs.readFileSync(path.join(__dirname, './template.html'), 'utf8')
|
const templatePath = path.join(process.cwd(), 'src/email/template.ejs')
|
||||||
: ''
|
const templateContent = fs.readFileSync(templatePath, 'utf8')
|
||||||
|
|
||||||
// Compile the template
|
// Compile and render the template with EJS
|
||||||
const getHTML = Handlebars.compile(template)
|
const preInlinedCSS = ejs.render(templateContent, { ...data, cta: data.cta || {} })
|
||||||
|
|
||||||
const generateEmailHTML = async (data): Promise<string> => {
|
// Inline CSS
|
||||||
const preInlinedCSS = getHTML(data)
|
const html = juice(preInlinedCSS)
|
||||||
|
|
||||||
const html = await inlineCSS(preInlinedCSS, {
|
return Promise.resolve(html)
|
||||||
url: ' ',
|
|
||||||
removeStyleTags: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
return html
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default generateEmailHTML
|
|
||||||
|
|||||||
@@ -1,13 +1,24 @@
|
|||||||
import generateEmailHTML from './generateEmailHTML'
|
import type { PayloadRequest } from 'payload'
|
||||||
|
|
||||||
const generateForgotPasswordEmail = async ({ token }): Promise<string> =>
|
import { generateEmailHTML } from './generateEmailHTML'
|
||||||
generateEmailHTML({
|
|
||||||
headline: 'Locked out?',
|
type ForgotPasswordEmailArgs =
|
||||||
|
| {
|
||||||
|
req?: PayloadRequest
|
||||||
|
token?: string
|
||||||
|
user?: any
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
|
||||||
|
export const generateForgotPasswordEmail = async (
|
||||||
|
args: ForgotPasswordEmailArgs,
|
||||||
|
): Promise<string> => {
|
||||||
|
return generateEmailHTML({
|
||||||
content: '<p>Let's get you back in.</p>',
|
content: '<p>Let's get you back in.</p>',
|
||||||
cta: {
|
cta: {
|
||||||
buttonLabel: 'Reset your password',
|
buttonLabel: 'Reset your password',
|
||||||
url: `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/reset-password?token=${token}`,
|
url: `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/reset-password?token=${args?.token}`,
|
||||||
},
|
},
|
||||||
|
headline: 'Locked out?',
|
||||||
})
|
})
|
||||||
|
}
|
||||||
export default generateForgotPasswordEmail
|
|
||||||
|
|||||||
@@ -1,16 +1,26 @@
|
|||||||
import generateEmailHTML from './generateEmailHTML'
|
import { generateEmailHTML } from './generateEmailHTML'
|
||||||
|
|
||||||
const generateVerificationEmail = async (args): Promise<string> => {
|
type User = {
|
||||||
const { user, token } = args
|
email: string
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GenerateVerificationEmailArgs = {
|
||||||
|
token: string
|
||||||
|
user: User
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateVerificationEmail = async (
|
||||||
|
args: GenerateVerificationEmailArgs,
|
||||||
|
): Promise<string> => {
|
||||||
|
const { token, user } = args
|
||||||
|
|
||||||
return generateEmailHTML({
|
return generateEmailHTML({
|
||||||
headline: 'Verify your account',
|
|
||||||
content: `<p>Hi${user.name ? ' ' + user.name : ''}! Validate your account by clicking the button below.</p>`,
|
content: `<p>Hi${user.name ? ' ' + user.name : ''}! Validate your account by clicking the button below.</p>`,
|
||||||
cta: {
|
cta: {
|
||||||
buttonLabel: 'Verify',
|
buttonLabel: 'Verify',
|
||||||
url: `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/verify?token=${token}&email=${user.email}`,
|
url: `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/verify?token=${token}&email=${user.email}`,
|
||||||
},
|
},
|
||||||
|
headline: 'Verify your account',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default generateVerificationEmail
|
|
||||||
|
|||||||
327
examples/email/src/email/template.ejs
Normal file
327
examples/email/src/email/template.ejs
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<style type="text/css">
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
html,
|
||||||
|
.bg {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
p,
|
||||||
|
em,
|
||||||
|
strong {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #333333;
|
||||||
|
outline: 0;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
a img {
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5 {
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 40px;
|
||||||
|
color: #333333;
|
||||||
|
margin: 0 0 25px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #333333;
|
||||||
|
margin: 0 0 25px 0;
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 25px;
|
||||||
|
color: #333333;
|
||||||
|
margin: 0 0 25px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #333333;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
color: #333333;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 900;
|
||||||
|
margin: 0 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
td {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 25px;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding-left: 15px;
|
||||||
|
margin-left: 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 25px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 25px;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.hr td {
|
||||||
|
font-size: 0;
|
||||||
|
line-height: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.white {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/********************************
|
||||||
|
MAIN
|
||||||
|
********************************/
|
||||||
|
|
||||||
|
.main {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/********************************
|
||||||
|
MAX WIDTHS
|
||||||
|
********************************/
|
||||||
|
|
||||||
|
.max-width {
|
||||||
|
max-width: 800px;
|
||||||
|
width: 94%;
|
||||||
|
margin: 0 3%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/********************************
|
||||||
|
REUSABLES
|
||||||
|
********************************/
|
||||||
|
|
||||||
|
.padding {
|
||||||
|
padding: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-border {
|
||||||
|
border: 0;
|
||||||
|
outline: none;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-margin {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
line-height: 45px;
|
||||||
|
height: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/********************************
|
||||||
|
PANELS
|
||||||
|
********************************/
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 800px) {
|
||||||
|
h1 {
|
||||||
|
font-size: 24px !important;
|
||||||
|
margin: 0 0 20px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 20px !important;
|
||||||
|
margin: 0 0 20px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 20px !important;
|
||||||
|
margin: 0 0 20px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 18px !important;
|
||||||
|
margin: 0 0 15px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 15px !important;
|
||||||
|
margin: 0 0 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-width {
|
||||||
|
width: 90% !important;
|
||||||
|
margin: 0 5% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.padding {
|
||||||
|
padding: 30px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.padding-vert {
|
||||||
|
padding-top: 20px !important;
|
||||||
|
padding-bottom: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.padding-horiz {
|
||||||
|
padding-left: 20px !important;
|
||||||
|
padding-right: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
line-height: 20px !important;
|
||||||
|
height: 20px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div style="background-color: #f3f3f3; height: 100%">
|
||||||
|
<table height="100%" width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#f3f3f3">
|
||||||
|
<tr>
|
||||||
|
<td valign="top" align="left">
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top">
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table class="max-width" cellpadding="0" cellspacing="0" border="0" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="spacer"> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="padding main">
|
||||||
|
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<!-- LOGO -->
|
||||||
|
<a href="https://payloadcms.com/" target="_blank">
|
||||||
|
<img src="https://payloadcms.com/images/logo-dark.png" width="150"
|
||||||
|
height="auto" />
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="spacer"> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<!-- HEADLINE -->
|
||||||
|
<h1 style="margin: 0 0 30px"><%= headline %></h1>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<%- content %>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<% if (cta) { %>
|
||||||
|
<div>
|
||||||
|
<a href="<%= cta.url %>" style="
|
||||||
|
background-color: #222222;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #ffffff;
|
||||||
|
display: inline-block;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 60px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
width: 200px;
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
">
|
||||||
|
<%= cta.buttonLabel %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1,345 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<style type="text/css">
|
|
||||||
body,
|
|
||||||
html {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body,
|
|
||||||
html,
|
|
||||||
.bg {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body,
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
p,
|
|
||||||
em,
|
|
||||||
strong {
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-size: 15px;
|
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #333333;
|
|
||||||
outline: 0;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
a img {
|
|
||||||
border: 0;
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5 {
|
|
||||||
font-weight: 900;
|
|
||||||
line-height: 1.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 40px;
|
|
||||||
color: #333333;
|
|
||||||
margin: 0 0 25px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
color: #333333;
|
|
||||||
margin: 0 0 25px 0;
|
|
||||||
font-size: 30px;
|
|
||||||
line-height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 25px;
|
|
||||||
color: #333333;
|
|
||||||
margin: 0 0 25px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
font-size: 20px;
|
|
||||||
color: #333333;
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
line-height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h5 {
|
|
||||||
color: #333333;
|
|
||||||
font-size: 17px;
|
|
||||||
font-weight: 900;
|
|
||||||
margin: 0 0 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
p,
|
|
||||||
td {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 25px;
|
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0 0 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
padding-left: 15px;
|
|
||||||
margin-left: 15px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 25px;
|
|
||||||
margin-bottom: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 25px;
|
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.hr td {
|
|
||||||
font-size: 0;
|
|
||||||
line-height: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.white {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/********************************
|
|
||||||
MAIN
|
|
||||||
********************************/
|
|
||||||
|
|
||||||
.main {
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/********************************
|
|
||||||
MAX WIDTHS
|
|
||||||
********************************/
|
|
||||||
|
|
||||||
.max-width {
|
|
||||||
max-width: 800px;
|
|
||||||
width: 94%;
|
|
||||||
margin: 0 3%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/********************************
|
|
||||||
REUSABLES
|
|
||||||
********************************/
|
|
||||||
|
|
||||||
.padding {
|
|
||||||
padding: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-border {
|
|
||||||
border: 0;
|
|
||||||
outline: none;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-margin {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spacer {
|
|
||||||
line-height: 45px;
|
|
||||||
height: 45px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/********************************
|
|
||||||
PANELS
|
|
||||||
********************************/
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 800px) {
|
|
||||||
h1 {
|
|
||||||
font-size: 24px !important;
|
|
||||||
margin: 0 0 20px 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 20px !important;
|
|
||||||
margin: 0 0 20px 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 20px !important;
|
|
||||||
margin: 0 0 20px 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
font-size: 18px !important;
|
|
||||||
margin: 0 0 15px 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
h5 {
|
|
||||||
font-size: 15px !important;
|
|
||||||
margin: 0 0 10px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.max-width {
|
|
||||||
width: 90% !important;
|
|
||||||
margin: 0 5% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
td.padding {
|
|
||||||
padding: 30px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
td.padding-vert {
|
|
||||||
padding-top: 20px !important;
|
|
||||||
padding-bottom: 20px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
td.padding-horiz {
|
|
||||||
padding-left: 20px !important;
|
|
||||||
padding-right: 20px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spacer {
|
|
||||||
line-height: 20px !important;
|
|
||||||
height: 20px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div style="background-color: #f3f3f3; height: 100%">
|
|
||||||
<table
|
|
||||||
height="100%"
|
|
||||||
width="100%"
|
|
||||||
cellpadding="0"
|
|
||||||
cellspacing="0"
|
|
||||||
border="0"
|
|
||||||
bgcolor="#f3f3f3"
|
|
||||||
style="background-color: #f3f3f3"
|
|
||||||
>
|
|
||||||
<tr>
|
|
||||||
<td valign="top" align="left">
|
|
||||||
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td align="center" valign="top">
|
|
||||||
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td align="center">
|
|
||||||
<table
|
|
||||||
class="max-width"
|
|
||||||
cellpadding="0"
|
|
||||||
cellspacing="0"
|
|
||||||
border="0"
|
|
||||||
width="100%"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td class="spacer"> </td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="padding main">
|
|
||||||
<table cellpadding="0" cellspacing="0" border="0" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<!-- LOGO -->
|
|
||||||
<a href="https://payloadcms.com/" target="_blank">
|
|
||||||
<img
|
|
||||||
src="https://payloadcms.com/images/logo-dark.png"
|
|
||||||
width="150"
|
|
||||||
height="auto"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="spacer"> </td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<!-- HEADLINE -->
|
|
||||||
<h1 style="margin: 0 0 30px">{{headline}}</h1>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<!-- CONTENT -->
|
|
||||||
{{{content}}}
|
|
||||||
|
|
||||||
<!-- CTA -->
|
|
||||||
{{#if cta}}
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="{{cta.url}}"
|
|
||||||
style="
|
|
||||||
background-color: #222222;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #ffffff;
|
|
||||||
display: inline-block;
|
|
||||||
font-family: sans-serif;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: bold;
|
|
||||||
line-height: 60px;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
width: 200px;
|
|
||||||
-webkit-text-size-adjust: none;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{cta.buttonLabel}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
let email
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
email = {
|
|
||||||
fromName: 'Payload',
|
|
||||||
fromAddress: 'info@payloadcms.com',
|
|
||||||
transportOptions: {
|
|
||||||
// Configure a custom transport here
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
email = {
|
|
||||||
fromName: 'Ethereal Email',
|
|
||||||
fromAddress: 'example@ethereal.com',
|
|
||||||
logMockCredentials: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default email
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export default {}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
/**
|
/**
|
||||||
* This file was automatically generated by Payload.
|
* This file was automatically generated by Payload.
|
||||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||||
@@ -6,30 +7,213 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
|
auth: {
|
||||||
|
users: UserAuthOperations;
|
||||||
|
};
|
||||||
collections: {
|
collections: {
|
||||||
'newsletter-signups': NewsletterSignup;
|
'newsletter-signups': NewsletterSignup;
|
||||||
users: User;
|
users: User;
|
||||||
|
'payload-locked-documents': PayloadLockedDocument;
|
||||||
|
'payload-preferences': PayloadPreference;
|
||||||
|
'payload-migrations': PayloadMigration;
|
||||||
|
};
|
||||||
|
collectionsJoins: {};
|
||||||
|
collectionsSelect: {
|
||||||
|
'newsletter-signups': NewsletterSignupsSelect<false> | NewsletterSignupsSelect<true>;
|
||||||
|
users: UsersSelect<false> | UsersSelect<true>;
|
||||||
|
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||||
|
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||||
|
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||||
|
};
|
||||||
|
db: {
|
||||||
|
defaultIDType: string;
|
||||||
};
|
};
|
||||||
globals: {};
|
globals: {};
|
||||||
|
globalsSelect: {};
|
||||||
|
locale: null;
|
||||||
|
user: User & {
|
||||||
|
collection: 'users';
|
||||||
|
};
|
||||||
|
jobs?: {
|
||||||
|
tasks: unknown;
|
||||||
|
workflows?: unknown;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
export interface UserAuthOperations {
|
||||||
|
forgotPassword: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
login: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
registerFirstUser: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
unlock: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "newsletter-signups".
|
||||||
|
*/
|
||||||
export interface NewsletterSignup {
|
export interface NewsletterSignup {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string | null;
|
||||||
email: string;
|
email: string;
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "users".
|
||||||
|
*/
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string | null;
|
||||||
email?: string;
|
|
||||||
resetPasswordToken?: string;
|
|
||||||
resetPasswordExpiration?: string;
|
|
||||||
_verified?: boolean;
|
|
||||||
_verificationToken?: string;
|
|
||||||
loginAttempts?: number;
|
|
||||||
lockUntil?: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
password?: string;
|
createdAt: string;
|
||||||
|
email: string;
|
||||||
|
resetPasswordToken?: string | null;
|
||||||
|
resetPasswordExpiration?: string | null;
|
||||||
|
salt?: string | null;
|
||||||
|
hash?: string | null;
|
||||||
|
_verified?: boolean | null;
|
||||||
|
_verificationToken?: string | null;
|
||||||
|
loginAttempts?: number | null;
|
||||||
|
lockUntil?: string | null;
|
||||||
|
password?: string | null;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-locked-documents".
|
||||||
|
*/
|
||||||
|
export interface PayloadLockedDocument {
|
||||||
|
id: string;
|
||||||
|
document?:
|
||||||
|
| ({
|
||||||
|
relationTo: 'newsletter-signups';
|
||||||
|
value: string | NewsletterSignup;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'users';
|
||||||
|
value: string | User;
|
||||||
|
} | null);
|
||||||
|
globalSlug?: string | null;
|
||||||
|
user: {
|
||||||
|
relationTo: 'users';
|
||||||
|
value: string | User;
|
||||||
|
};
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-preferences".
|
||||||
|
*/
|
||||||
|
export interface PayloadPreference {
|
||||||
|
id: string;
|
||||||
|
user: {
|
||||||
|
relationTo: 'users';
|
||||||
|
value: string | User;
|
||||||
|
};
|
||||||
|
key?: string | null;
|
||||||
|
value?:
|
||||||
|
| {
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
| unknown[]
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-migrations".
|
||||||
|
*/
|
||||||
|
export interface PayloadMigration {
|
||||||
|
id: string;
|
||||||
|
name?: string | null;
|
||||||
|
batch?: number | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "newsletter-signups_select".
|
||||||
|
*/
|
||||||
|
export interface NewsletterSignupsSelect<T extends boolean = true> {
|
||||||
|
name?: T;
|
||||||
|
email?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "users_select".
|
||||||
|
*/
|
||||||
|
export interface UsersSelect<T extends boolean = true> {
|
||||||
|
name?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
email?: T;
|
||||||
|
resetPasswordToken?: T;
|
||||||
|
resetPasswordExpiration?: T;
|
||||||
|
salt?: T;
|
||||||
|
hash?: T;
|
||||||
|
_verified?: T;
|
||||||
|
_verificationToken?: T;
|
||||||
|
loginAttempts?: T;
|
||||||
|
lockUntil?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-locked-documents_select".
|
||||||
|
*/
|
||||||
|
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||||
|
document?: T;
|
||||||
|
globalSlug?: T;
|
||||||
|
user?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-preferences_select".
|
||||||
|
*/
|
||||||
|
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||||
|
user?: T;
|
||||||
|
key?: T;
|
||||||
|
value?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "payload-migrations_select".
|
||||||
|
*/
|
||||||
|
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||||
|
name?: T;
|
||||||
|
batch?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "auth".
|
||||||
|
*/
|
||||||
|
export interface Auth {
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
declare module 'payload' {
|
||||||
|
export interface GeneratedTypes extends Config {}
|
||||||
|
}
|
||||||
@@ -1,42 +1,37 @@
|
|||||||
import dotenv from 'dotenv'
|
import { mongooseAdapter } from '@payloadcms/db-mongodb'
|
||||||
|
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||||
|
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { buildConfig } from 'payload/config'
|
import { buildConfig } from 'payload'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
import Users from './collections/Users'
|
import { Newsletter } from './collections/Newsletter'
|
||||||
import Newsletter from './collections/Newsletter'
|
import { Users } from './collections/Users'
|
||||||
|
|
||||||
dotenv.config({
|
const filename = fileURLToPath(import.meta.url)
|
||||||
path: path.resolve(__dirname, '../.env'),
|
const dirname = path.dirname(filename)
|
||||||
})
|
|
||||||
|
|
||||||
const mockModulePath = path.resolve(__dirname, './emptyModule.js')
|
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-exports
|
||||||
export default buildConfig({
|
export default buildConfig({
|
||||||
admin: {
|
admin: {
|
||||||
webpack: (config) => ({
|
importMap: {
|
||||||
...config,
|
baseDir: path.resolve(dirname),
|
||||||
resolve: {
|
},
|
||||||
...config?.resolve,
|
user: Users.slug,
|
||||||
alias: [
|
|
||||||
'fs',
|
|
||||||
'handlebars',
|
|
||||||
'inline-css',
|
|
||||||
path.resolve(__dirname, './email/transport'),
|
|
||||||
path.resolve(__dirname, './email/generateEmailHTML'),
|
|
||||||
path.resolve(__dirname, './email/generateForgotPasswordEmail'),
|
|
||||||
path.resolve(__dirname, './email/generateVerificationEmail'),
|
|
||||||
].reduce(
|
|
||||||
(aliases, importPath) => ({
|
|
||||||
...aliases,
|
|
||||||
[importPath]: mockModulePath,
|
|
||||||
}),
|
|
||||||
config.resolve.alias,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
collections: [Newsletter, Users],
|
collections: [Newsletter, Users],
|
||||||
|
db: mongooseAdapter({
|
||||||
|
url: process.env.DATABASE_URI || '',
|
||||||
|
}),
|
||||||
|
editor: lexicalEditor({}),
|
||||||
|
// For example use case, we are passing nothing to nodemailerAdapter
|
||||||
|
// This will default to using etherial.email
|
||||||
|
email: nodemailerAdapter(),
|
||||||
|
graphQL: {
|
||||||
|
schemaOutputFile: path.resolve(dirname, 'generated-schema.graphql'),
|
||||||
|
},
|
||||||
|
secret: process.env.PAYLOAD_SECRET || '',
|
||||||
typescript: {
|
typescript: {
|
||||||
outputFile: path.resolve(__dirname, 'payload-types.ts'),
|
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import express from 'express'
|
|
||||||
import path from 'path'
|
|
||||||
import payload from 'payload'
|
|
||||||
import email from './email/transport'
|
|
||||||
|
|
||||||
require('dotenv').config({
|
|
||||||
path: path.resolve(__dirname, '../.env'),
|
|
||||||
})
|
|
||||||
|
|
||||||
const app = express()
|
|
||||||
|
|
||||||
app.get('/', (_, res) => {
|
|
||||||
res.redirect('/admin')
|
|
||||||
})
|
|
||||||
|
|
||||||
const start = async (): Promise<void> => {
|
|
||||||
await payload.init({
|
|
||||||
secret: process.env.PAYLOAD_SECRET,
|
|
||||||
mongoURL: process.env.MONGODB_URI,
|
|
||||||
express: app,
|
|
||||||
email,
|
|
||||||
onInit: () => {
|
|
||||||
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
app.listen(8000)
|
|
||||||
}
|
|
||||||
|
|
||||||
start()
|
|
||||||
@@ -1,24 +1,48 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"baseUrl": ".",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"strict": false,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"outDir": "./dist",
|
"strict": true,
|
||||||
"rootDir": "./src",
|
"noEmit": true,
|
||||||
"jsx": "react",
|
"esModuleInterop": true,
|
||||||
"sourceMap": true,
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"payload/generated-types": ["./src/payload-types.ts"],
|
"@/*": [
|
||||||
"node_modules/*": ["./node_modules/*"]
|
"./src/*"
|
||||||
}
|
],
|
||||||
|
"@payload-config": [
|
||||||
|
"src/payload.config.ts"
|
||||||
|
],
|
||||||
|
"@payload-types": [
|
||||||
|
"src/payload-types.ts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"target": "ES2017"
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": [
|
||||||
"exclude": ["node_modules", "dist", "build"],
|
"next-env.d.ts",
|
||||||
"ts-node": {
|
"**/*.ts",
|
||||||
"transpileOnly": true
|
"**/*.tsx",
|
||||||
}
|
".next/types/**/*.ts",
|
||||||
|
"src/mocks/emptyObject.js"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user