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:
Patrik
2024-11-15 14:10:24 -05:00
committed by GitHub
parent 7c732bec14
commit ba06ce6338
32 changed files with 7811 additions and 8510 deletions

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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
View 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.

View 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)

View File

@@ -1,5 +0,0 @@
{
"ext": "ts",
"exec": "ts-node src/server.ts -- -I",
"stdin": false
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1 @@
export const importMap = {}

View 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)

View File

@@ -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)

View 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)

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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&apos;s get you back in.</p>', content: '<p>Let&apos;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

View File

@@ -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

View 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">&nbsp;</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">&nbsp;</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>

View File

@@ -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">&nbsp;</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">&nbsp;</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>

View File

@@ -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

View File

@@ -1 +0,0 @@
export default {}

View File

@@ -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 {}
}

View File

@@ -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'),
}, },
}) })

View File

@@ -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()

View File

@@ -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