chore(examples): removes external auth examples (#8605)
In effort to keep the Examples Directory as easy to navigate as possible, and to keep the Payload Monorepo only as verbose as it needs to be, we need to remove all alternatives from the Examples Directory. This includes setups that interact with Payload from a standalone server, keeping only the Payload recommended "combined" Next.js + Payload setups. This will also be applied to all other examples that use this setup, i.e. draft preview, live preview, etc.
This commit is contained in:
@@ -143,7 +143,7 @@ It is also possible to allow multiple user types into the Admin Panel with limit
|
|||||||
- `super-admin` - full access to the Admin Panel to perform any action
|
- `super-admin` - full access to the Admin Panel to perform any action
|
||||||
- `editor` - limited access to the Admin Panel to only manage content
|
- `editor` - limited access to the Admin Panel to only manage content
|
||||||
|
|
||||||
To do this, add a `roles` or similar field to your auth-enabled Collection, then use the `access.admin` property to grant or deny access based on the value of that field. See [Access Control](/docs/access-control/overview) for full details. For a complete, working example of role-based access control, check out the official [Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth/payload).
|
To do this, add a `roles` or similar field to your auth-enabled Collection, then use the `access.admin` property to grant or deny access based on the value of that field. See [Access Control](/docs/access-control/overview) for full details. For a complete, working example of role-based access control, check out the official [Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth).
|
||||||
|
|
||||||
## Customizing Routes
|
## Customizing Routes
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ desc:
|
|||||||
keywords: example, examples, starter, boilerplate, template, templates
|
keywords: example, examples, starter, boilerplate, template, templates
|
||||||
---
|
---
|
||||||
|
|
||||||
Payload provides a vast array of examples to help you get started with your project no matter what you are working on. These examples are designed to be easy to get up and running, and to be easy to understand. They showcase nothing more than the specific features being demonstrated so you can easily decipher what is going on.
|
Payload provides a vast array of examples to help you get started with your project no matter what you are working on. These examples are designed to be easy to get up and running, and to be easy to understand. They showcase nothing more than the specific features being demonstrated so you can easily decipher precisely what is going on.
|
||||||
|
|
||||||
Examples are changing every day, so be sure to check back often to see what new examples have been added. If you have a specific example you would like to see, please feel free to start a new [Discussion](https://github.com/payloadcms/payload/discussions) or open a new [PR](https://github.com/payloadcms/payload/pulls) to add it yourself.
|
|
||||||
|
|
||||||
- [Auth](https://github.com/payloadcms/payload/tree/main/examples/auth)
|
- [Auth](https://github.com/payloadcms/payload/tree/main/examples/auth)
|
||||||
- [Custom Components](https://github.com/payloadcms/payload/tree/main/examples/custom-components)
|
- [Custom Components](https://github.com/payloadcms/payload/tree/main/examples/custom-components)
|
||||||
@@ -21,16 +19,4 @@ Examples are changing every day, so be sure to check back often to see what new
|
|||||||
- [Tests](https://github.com/payloadcms/payload/tree/main/examples/testing)
|
- [Tests](https://github.com/payloadcms/payload/tree/main/examples/testing)
|
||||||
- [White-label Admin UI](https://github.com/payloadcms/payload/tree/main/examples/whitelabel)
|
- [White-label Admin UI](https://github.com/payloadcms/payload/tree/main/examples/whitelabel)
|
||||||
|
|
||||||
When necessary, some examples include a front-end. Examples that require a front-end share this folder structure:
|
We are adding new examples every day, so if your particular use case is not demonstrated in any existing example, please feel free to start a new [Discussion](https://github.com/payloadcms/payload/discussions) or open a new [PR](https://github.com/payloadcms/payload/pulls) to add it yourself.
|
||||||
|
|
||||||
```plaintext
|
|
||||||
example/
|
|
||||||
├── payload/
|
|
||||||
├── next-app/
|
|
||||||
├── next-pages/
|
|
||||||
├── react-router/
|
|
||||||
├── vue/
|
|
||||||
├── svelte/
|
|
||||||
```
|
|
||||||
|
|
||||||
Where `payload` is your Payload project, and the other directories are dedicated to their respective front-end framework. We are adding new examples every day, so if your framework of choice is not yet supported in any particular example, please feel free to start a new [Discussion](https://github.com/payloadcms/payload/discussions) or open a new [PR](https://github.com/payloadcms/payload/pulls) to add it yourself.
|
|
||||||
|
|||||||
@@ -2,14 +2,7 @@
|
|||||||
|
|
||||||
This [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth) demonstrates how to implement [Payload Authentication](https://payloadcms.com/docs/authentication/overview) into all types of applications. Follow the [Quick Start](#quick-start) to get up and running quickly.
|
This [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth) demonstrates how to implement [Payload Authentication](https://payloadcms.com/docs/authentication/overview) into all types of applications. Follow the [Quick Start](#quick-start) to get up and running quickly.
|
||||||
|
|
||||||
**IMPORTANT—This example includes a fully integrated Next.js App Router front-end that runs on the same server as Payload.** If you are working on an application running on an entirely separate server, there are various fully working, separately running front-ends made explicitly for this example, including:
|
**IMPORTANT—This example includes a fully integrated Next.js App Router front-end that runs on the same server as Payload.** If you are working on an application running on an entirely separate server, the principals are generally the same. To learn more about this, [check out how Payload can be used in its various headless capacities](https://payloadcms.com/blog/the-ultimate-guide-to-using-nextjs-with-payload).
|
||||||
|
|
||||||
- [Next.js App Router](../next-app)
|
|
||||||
- [Next.js Pages Router](../next-pages)
|
|
||||||
|
|
||||||
Those applications run directly alongside this one. Follow the instructions in each respective README to get started. If you are setting up authentication for another front-end, please consider contributing to this repo with your own example!
|
|
||||||
|
|
||||||
To learn more about this, [check out how Payload can be used in its various headless capacities](https://payloadcms.com/blog/the-ultimate-guide-to-using-nextjs-with-payload).
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -132,7 +125,3 @@ If you are using an integrated Next.js setup, the easiest way to deploy your Nex
|
|||||||
## Questions
|
## Questions
|
||||||
|
|
||||||
If you have any issues or questions, reach out to us on [Discord](https://discord.com/invite/payload) or start a [GitHub discussion](https://github.com/payloadcms/payload/discussions).
|
If you have any issues or questions, reach out to us on [Discord](https://discord.com/invite/payload) or start a [GitHub discussion](https://github.com/payloadcms/payload/discussions).
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
NEXT_PUBLIC_PAYLOAD_URL=http://localhost:3000
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
extends: ['plugin:@next/next/recommended', '@payloadcms'],
|
|
||||||
rules: {
|
|
||||||
'import/extensions': 'off',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
6
examples/auth/next-app/.gitignore
vendored
6
examples/auth/next-app/.gitignore
vendored
@@ -1,6 +0,0 @@
|
|||||||
.next
|
|
||||||
dist
|
|
||||||
build
|
|
||||||
node_modules
|
|
||||||
.env
|
|
||||||
package-lock.json
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# Payload Auth Example Front-End
|
|
||||||
|
|
||||||
This is a [Next.js](https://nextjs.org) [App Router](https://nextjs.org/docs/app) front-end made explicitly for the [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth). This example demonstrates how to authenticate your Next.js app using [Payload Authentication](https://payloadcms.com/docs/authentication/overview).
|
|
||||||
|
|
||||||
> This example uses the App Router, the latest API of Next.js. If your app is using the legacy [Pages Router](https://nextjs.org/docs/pages), check out the official [Pages Router Example](https://github.com/payloadcms/payload/tree/main/examples/auth/next-pages).
|
|
||||||
|
|
||||||
**IMPORTANT—This application runs on a different server as Payload and establishes a connection from another domain or port over HTTP.** For an integrated setup that runs on a single server and uses the [Local API](https://payloadcms.com/docs/local-api/overview#local-api), check out [how to serve Payload alongside Next.js](https://github.com/payloadcms/payload/tree/main/examples/auth/payload). To learn more about this, check out [how Payload can be used in its various headless capacities](https://payloadcms.com/blog/the-ultimate-guide-to-using-nextjs-with-payload).
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Payload
|
|
||||||
|
|
||||||
First you'll need a running Payload app. There is one made explicitly for this example and [can be found here](https://github.com/payloadcms/payload/tree/main/examples/auth/payload). If you have not done so already, clone it down and follow the setup instructions there. This will provide all the necessary APIs that your Next.js app requires for authentication.
|
|
||||||
|
|
||||||
### Next.js
|
|
||||||
|
|
||||||
1. Clone this repo
|
|
||||||
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
|
|
||||||
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
|
|
||||||
3. `cp .env.example .env` to copy the example environment variables
|
|
||||||
4. `pnpm dev`, `yarn dev`, or `npm run dev` to start the server
|
|
||||||
5. `open http://localhost:3001` to see the result
|
|
||||||
|
|
||||||
Once running, a user is automatically seeded in your local environment with some basic instructions. See the [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth) for full details.
|
|
||||||
|
|
||||||
## Learn More
|
|
||||||
|
|
||||||
To learn more about Payload and Next.js, take a look at the following resources:
|
|
||||||
|
|
||||||
- [Payload Documentation](https://payloadcms.com/docs) - learn about Payload features and API.
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
||||||
|
|
||||||
You can check out [the Payload GitHub repository](https://github.com/payloadcms/payload) as well as [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new) from the creators of Next.js. You could also combine this app into a [single Express server](https://github.com/payloadcms/payload/tree/main/examples/custom-server) and deploy in to [Payload Cloud](https://payloadcms.com/new/import).
|
|
||||||
|
|
||||||
Check out our [Payload deployment documentation](https://payloadcms.com/docs/production/deployment) or the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
|
||||||
|
|
||||||
## Questions
|
|
||||||
|
|
||||||
If you have any issues or questions, reach out to us on [Discord](https://discord.com/invite/payload) or start a [GitHub discussion](https://github.com/payloadcms/payload/discussions).
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { ElementType } from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
export type Props = {
|
|
||||||
label?: string
|
|
||||||
appearance?: 'default' | 'primary' | 'secondary'
|
|
||||||
el?: 'button' | 'link' | 'a'
|
|
||||||
onClick?: () => void
|
|
||||||
href?: string
|
|
||||||
newTab?: boolean
|
|
||||||
className?: string
|
|
||||||
type?: 'submit' | 'button'
|
|
||||||
disabled?: boolean
|
|
||||||
invert?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Button: React.FC<Props> = ({
|
|
||||||
el: elFromProps = 'link',
|
|
||||||
label,
|
|
||||||
newTab,
|
|
||||||
href,
|
|
||||||
appearance,
|
|
||||||
className: classNameFromProps,
|
|
||||||
onClick,
|
|
||||||
type = 'button',
|
|
||||||
disabled,
|
|
||||||
invert,
|
|
||||||
}) => {
|
|
||||||
let el = elFromProps
|
|
||||||
const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {}
|
|
||||||
|
|
||||||
const className = [
|
|
||||||
classes.button,
|
|
||||||
classNameFromProps,
|
|
||||||
classes[`appearance--${appearance}`],
|
|
||||||
invert && classes[`${appearance}--invert`],
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ')
|
|
||||||
|
|
||||||
const content = (
|
|
||||||
<div className={classes.content}>
|
|
||||||
<span className={classes.label}>{label}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (onClick || type === 'submit') el = 'button'
|
|
||||||
|
|
||||||
if (el === 'link') {
|
|
||||||
return (
|
|
||||||
<Link href={href || ''} className={className} {...newTabProps} onClick={onClick}>
|
|
||||||
{content}
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Element: ElementType = el
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Element
|
|
||||||
href={href}
|
|
||||||
className={className}
|
|
||||||
type={type}
|
|
||||||
{...newTabProps}
|
|
||||||
onClick={onClick}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</Element>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import React, { forwardRef, Ref } from 'react'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
left?: boolean
|
|
||||||
right?: boolean
|
|
||||||
className?: string
|
|
||||||
children: React.ReactNode
|
|
||||||
ref?: Ref<HTMLDivElement>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Gutter: React.FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
|
||||||
const { left = true, right = true, className, children } = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={[
|
|
||||||
classes.gutter,
|
|
||||||
left && classes.gutterLeft,
|
|
||||||
right && classes.gutterRight,
|
|
||||||
className,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ')}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
Gutter.displayName = 'Gutter'
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
import { useAuth } from '../../../_providers/Auth'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
export const HeaderNav: React.FC = () => {
|
|
||||||
const { user } = useAuth()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav
|
|
||||||
className={[
|
|
||||||
classes.nav,
|
|
||||||
// fade the nav in on user load to avoid flash of content and layout shift
|
|
||||||
// Vercel also does this in their own website header, see https://vercel.com
|
|
||||||
user === undefined && classes.hide,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ')}
|
|
||||||
>
|
|
||||||
{user && (
|
|
||||||
<React.Fragment>
|
|
||||||
<Link href="/account">Account</Link>
|
|
||||||
<Link href="/logout">Logout</Link>
|
|
||||||
</React.Fragment>
|
|
||||||
)}
|
|
||||||
{!user && (
|
|
||||||
<React.Fragment>
|
|
||||||
<Link href="/login">Login</Link>
|
|
||||||
<Link href="/create-account">Create Account</Link>
|
|
||||||
</React.Fragment>
|
|
||||||
)}
|
|
||||||
</nav>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import Image from 'next/image'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
import { Gutter } from '../Gutter'
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
import { HeaderNav } from './Nav'
|
|
||||||
|
|
||||||
export function Header() {
|
|
||||||
return (
|
|
||||||
<header className={classes.header}>
|
|
||||||
<Gutter className={classes.wrap}>
|
|
||||||
<Link className={classes.logo} href="/">
|
|
||||||
<picture>
|
|
||||||
<source
|
|
||||||
media="(prefers-color-scheme: dark)"
|
|
||||||
srcSet="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-logo-light.svg"
|
|
||||||
/>
|
|
||||||
<Image
|
|
||||||
alt="Payload Logo"
|
|
||||||
height={30}
|
|
||||||
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-logo-dark.svg"
|
|
||||||
width={150}
|
|
||||||
/>
|
|
||||||
</picture>
|
|
||||||
</Link>
|
|
||||||
<HeaderNav />
|
|
||||||
</Gutter>
|
|
||||||
</header>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Header
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { FieldValues, UseFormRegister, Validate } from 'react-hook-form'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
name: string
|
|
||||||
label: string
|
|
||||||
register: UseFormRegister<FieldValues & any>
|
|
||||||
required?: boolean
|
|
||||||
error: any
|
|
||||||
type?: 'text' | 'number' | 'password' | 'email'
|
|
||||||
validate?: (value: string) => boolean | string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Input: React.FC<Props> = ({
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
required,
|
|
||||||
register,
|
|
||||||
error,
|
|
||||||
type = 'text',
|
|
||||||
validate,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className={classes.inputWrap}>
|
|
||||||
<label htmlFor="name" className={classes.label}>
|
|
||||||
{`${label} ${required ? '*' : ''}`}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
className={[classes.input, error && classes.error].filter(Boolean).join(' ')}
|
|
||||||
{...{ type }}
|
|
||||||
{...register(name, {
|
|
||||||
required,
|
|
||||||
validate,
|
|
||||||
...(type === 'email'
|
|
||||||
? {
|
|
||||||
pattern: {
|
|
||||||
value: /\S+@\S+\.\S+/,
|
|
||||||
message: 'Please enter a valid email',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
{error && (
|
|
||||||
<div className={classes.errorMessage}>
|
|
||||||
{!error?.message && error?.type === 'required'
|
|
||||||
? 'This field is required'
|
|
||||||
: error?.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
export const Message: React.FC<{
|
|
||||||
message?: React.ReactNode
|
|
||||||
error?: React.ReactNode
|
|
||||||
success?: React.ReactNode
|
|
||||||
warning?: React.ReactNode
|
|
||||||
className?: string
|
|
||||||
}> = ({ message, error, success, warning, className }) => {
|
|
||||||
const messageToRender = message || error || success || warning
|
|
||||||
|
|
||||||
if (messageToRender) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={[
|
|
||||||
classes.message,
|
|
||||||
className,
|
|
||||||
error && classes.error,
|
|
||||||
success && classes.success,
|
|
||||||
warning && classes.warning,
|
|
||||||
!error && !success && !warning && classes.default,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ')}
|
|
||||||
>
|
|
||||||
{messageToRender}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useSearchParams } from 'next/navigation'
|
|
||||||
|
|
||||||
import { Message } from '../Message'
|
|
||||||
|
|
||||||
export const RenderParams: React.FC<{
|
|
||||||
params?: string[]
|
|
||||||
message?: string
|
|
||||||
className?: string
|
|
||||||
}> = ({ params = ['error', 'message', 'success'], message, className }) => {
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const paramValues = params.map((param) => searchParams.get(param)).filter(Boolean)
|
|
||||||
|
|
||||||
if (paramValues.length) {
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
{paramValues.map((paramValue) => (
|
|
||||||
<Message
|
|
||||||
key={paramValue}
|
|
||||||
message={(message || 'PARAM')?.replace('PARAM', paramValue || '')}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
import serialize from './serialize'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
const RichText: React.FC<{ className?: string; content: any }> = ({ className, content }) => {
|
|
||||||
if (!content) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={[classes.richText, className].filter(Boolean).join(' ')}>
|
|
||||||
{serialize(content)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RichText
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import React, { Fragment } from 'react'
|
|
||||||
import escapeHTML from 'escape-html'
|
|
||||||
import { Text } from 'slate'
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-use-before-define
|
|
||||||
type Children = Leaf[]
|
|
||||||
|
|
||||||
type Leaf = {
|
|
||||||
type: string
|
|
||||||
value?: {
|
|
||||||
url: string
|
|
||||||
alt: string
|
|
||||||
}
|
|
||||||
children: Children
|
|
||||||
url?: string
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
const serialize = (children: Children): React.ReactNode[] =>
|
|
||||||
children.map((node, i) => {
|
|
||||||
if (Text.isText(node)) {
|
|
||||||
let text = <span dangerouslySetInnerHTML={{ __html: escapeHTML(node.text) }} />
|
|
||||||
|
|
||||||
if (node.bold) {
|
|
||||||
text = <strong key={i}>{text}</strong>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.code) {
|
|
||||||
text = <code key={i}>{text}</code>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.italic) {
|
|
||||||
text = <em key={i}>{text}</em>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.underline) {
|
|
||||||
text = (
|
|
||||||
<span style={{ textDecoration: 'underline' }} key={i}>
|
|
||||||
{text}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.strikethrough) {
|
|
||||||
text = (
|
|
||||||
<span style={{ textDecoration: 'line-through' }} key={i}>
|
|
||||||
{text}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Fragment key={i}>{text}</Fragment>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!node) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (node.type) {
|
|
||||||
case 'h1':
|
|
||||||
return <h1 key={i}>{serialize(node.children)}</h1>
|
|
||||||
case 'h2':
|
|
||||||
return <h2 key={i}>{serialize(node.children)}</h2>
|
|
||||||
case 'h3':
|
|
||||||
return <h3 key={i}>{serialize(node.children)}</h3>
|
|
||||||
case 'h4':
|
|
||||||
return <h4 key={i}>{serialize(node.children)}</h4>
|
|
||||||
case 'h5':
|
|
||||||
return <h5 key={i}>{serialize(node.children)}</h5>
|
|
||||||
case 'h6':
|
|
||||||
return <h6 key={i}>{serialize(node.children)}</h6>
|
|
||||||
case 'blockquote':
|
|
||||||
return <blockquote key={i}>{serialize(node.children)}</blockquote>
|
|
||||||
case 'ul':
|
|
||||||
return <ul key={i}>{serialize(node.children)}</ul>
|
|
||||||
case 'ol':
|
|
||||||
return <ol key={i}>{serialize(node.children)}</ol>
|
|
||||||
case 'li':
|
|
||||||
return <li key={i}>{serialize(node.children)}</li>
|
|
||||||
case 'link':
|
|
||||||
return (
|
|
||||||
<a href={escapeHTML(node.url)} key={i}>
|
|
||||||
{serialize(node.children)}
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return <p key={i}>{serialize(node.children)}</p>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default serialize
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
export const USER = `
|
|
||||||
id
|
|
||||||
email
|
|
||||||
firstName
|
|
||||||
lastName
|
|
||||||
`
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export const gql = async (query: string): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/graphql`, {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
query,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const { data, errors } = await res.json()
|
|
||||||
|
|
||||||
if (errors) {
|
|
||||||
throw new Error(errors[0].message)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.ok && data) {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
} catch (e: unknown) {
|
|
||||||
throw new Error(e as string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
import { User } from '../../payload-types'
|
|
||||||
import { gql, USER } from './gql'
|
|
||||||
import { rest } from './rest'
|
|
||||||
import { AuthContext, Create, ForgotPassword, Login, Logout, ResetPassword } from './types'
|
|
||||||
|
|
||||||
const Context = createContext({} as AuthContext)
|
|
||||||
|
|
||||||
export const AuthProvider: React.FC<{ children: React.ReactNode; api?: 'rest' | 'gql' }> = ({
|
|
||||||
children,
|
|
||||||
api = 'rest',
|
|
||||||
}) => {
|
|
||||||
const [user, setUser] = useState<User | null>()
|
|
||||||
|
|
||||||
const create = useCallback<Create>(
|
|
||||||
async (args) => {
|
|
||||||
if (api === 'rest') {
|
|
||||||
const user = await rest(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users`, args)
|
|
||||||
setUser(user)
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
if (api === 'gql') {
|
|
||||||
const { createUser: user } = await gql(`mutation {
|
|
||||||
createUser(data: { email: "${args.email}", password: "${args.password}", firstName: "${args.firstName}", lastName: "${args.lastName}" }) {
|
|
||||||
${USER}
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
setUser(user)
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[api],
|
|
||||||
)
|
|
||||||
|
|
||||||
const login = useCallback<Login>(
|
|
||||||
async (args) => {
|
|
||||||
if (api === 'rest') {
|
|
||||||
const user = await rest(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users/login`, args)
|
|
||||||
setUser(user)
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
if (api === 'gql') {
|
|
||||||
const { loginUser } = await gql(`mutation {
|
|
||||||
loginUser(email: "${args.email}", password: "${args.password}") {
|
|
||||||
user {
|
|
||||||
${USER}
|
|
||||||
}
|
|
||||||
exp
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
setUser(loginUser?.user)
|
|
||||||
return loginUser?.user
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[api],
|
|
||||||
)
|
|
||||||
|
|
||||||
const logout = useCallback<Logout>(async () => {
|
|
||||||
if (api === 'rest') {
|
|
||||||
await rest(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users/logout`)
|
|
||||||
setUser(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (api === 'gql') {
|
|
||||||
await gql(`mutation {
|
|
||||||
logoutUser
|
|
||||||
}`)
|
|
||||||
|
|
||||||
setUser(null)
|
|
||||||
}
|
|
||||||
}, [api])
|
|
||||||
|
|
||||||
// On mount, get user and set
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchMe = async () => {
|
|
||||||
if (api === 'rest') {
|
|
||||||
const user = await rest(
|
|
||||||
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users/me`,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
setUser(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (api === 'gql') {
|
|
||||||
const { meUser } = await gql(`query {
|
|
||||||
meUser {
|
|
||||||
user {
|
|
||||||
${USER}
|
|
||||||
}
|
|
||||||
exp
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
setUser(meUser.user)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchMe()
|
|
||||||
}, [api])
|
|
||||||
|
|
||||||
const forgotPassword = useCallback<ForgotPassword>(
|
|
||||||
async (args) => {
|
|
||||||
if (api === 'rest') {
|
|
||||||
const user = await rest(
|
|
||||||
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users/forgot-password`,
|
|
||||||
args,
|
|
||||||
)
|
|
||||||
setUser(user)
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
if (api === 'gql') {
|
|
||||||
const { forgotPasswordUser } = await gql(`mutation {
|
|
||||||
forgotPasswordUser(email: "${args.email}")
|
|
||||||
}`)
|
|
||||||
|
|
||||||
return forgotPasswordUser
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[api],
|
|
||||||
)
|
|
||||||
|
|
||||||
const resetPassword = useCallback<ResetPassword>(
|
|
||||||
async (args) => {
|
|
||||||
if (api === 'rest') {
|
|
||||||
const user = await rest(
|
|
||||||
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users/reset-password`,
|
|
||||||
args,
|
|
||||||
)
|
|
||||||
setUser(user)
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
if (api === 'gql') {
|
|
||||||
const { resetPasswordUser } = await gql(`mutation {
|
|
||||||
resetPasswordUser(password: "${args.password}", token: "${args.token}") {
|
|
||||||
user {
|
|
||||||
${USER}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
setUser(resetPasswordUser.user)
|
|
||||||
return resetPasswordUser.user
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[api],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Context.Provider
|
|
||||||
value={{
|
|
||||||
user,
|
|
||||||
setUser,
|
|
||||||
login,
|
|
||||||
logout,
|
|
||||||
create,
|
|
||||||
resetPassword,
|
|
||||||
forgotPassword,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Context.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type UseAuth<T = User> = () => AuthContext // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
export const useAuth: UseAuth = () => useContext(Context)
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import type { User } from '../../payload-types'
|
|
||||||
|
|
||||||
export const rest = async (
|
|
||||||
url: string,
|
|
||||||
args?: any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
||||||
options?: RequestInit,
|
|
||||||
): Promise<User | null | undefined> => {
|
|
||||||
const method = options?.method || 'POST'
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method,
|
|
||||||
...(method === 'POST' ? { body: JSON.stringify(args) } : {}),
|
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options?.headers,
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
|
|
||||||
const { errors, user } = await res.json()
|
|
||||||
|
|
||||||
if (errors) {
|
|
||||||
throw new Error(errors[0].message)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
} catch (e: unknown) {
|
|
||||||
throw new Error(e as string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import type { User } from '../../payload-types'
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
export type ResetPassword = (args: {
|
|
||||||
password: string
|
|
||||||
passwordConfirm: string
|
|
||||||
token: string
|
|
||||||
}) => Promise<User>
|
|
||||||
|
|
||||||
export type ForgotPassword = (args: { email: string }) => Promise<User> // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
export type Create = (args: {
|
|
||||||
email: string
|
|
||||||
password: string
|
|
||||||
firstName: string
|
|
||||||
lastName: string
|
|
||||||
}) => Promise<User> // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
export type Login = (args: { email: string; password: string }) => Promise<User> // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
export type Logout = () => Promise<void>
|
|
||||||
|
|
||||||
export interface AuthContext {
|
|
||||||
user?: User | null
|
|
||||||
setUser: (user: User | null) => void // eslint-disable-line no-unused-vars
|
|
||||||
logout: Logout
|
|
||||||
login: Login
|
|
||||||
create: Create
|
|
||||||
resetPassword: ResetPassword
|
|
||||||
forgotPassword: ForgotPassword
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { cookies } from 'next/headers'
|
|
||||||
import { redirect } from 'next/navigation'
|
|
||||||
|
|
||||||
import type { User } from '../payload-types'
|
|
||||||
|
|
||||||
export const getMeUser = async (args?: {
|
|
||||||
nullUserRedirect?: string
|
|
||||||
validUserRedirect?: string
|
|
||||||
}): Promise<{
|
|
||||||
user: User
|
|
||||||
token: string | undefined
|
|
||||||
}> => {
|
|
||||||
const { nullUserRedirect, validUserRedirect } = args || {}
|
|
||||||
const cookieStore = cookies()
|
|
||||||
const token = cookieStore.get('payload-token')?.value
|
|
||||||
|
|
||||||
const meUserReq = await fetch(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users/me`, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `JWT ${token}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const {
|
|
||||||
user,
|
|
||||||
}: {
|
|
||||||
user: User
|
|
||||||
} = await meUserReq.json()
|
|
||||||
|
|
||||||
if (validUserRedirect && meUserReq.ok && user) {
|
|
||||||
redirect(validUserRedirect)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nullUserRedirect && (!meUserReq.ok || !user)) {
|
|
||||||
redirect(nullUserRedirect)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
user,
|
|
||||||
token,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
import { useRouter } from 'next/navigation'
|
|
||||||
|
|
||||||
import { Button } from '../../_components/Button'
|
|
||||||
import { Input } from '../../_components/Input'
|
|
||||||
import { Message } from '../../_components/Message'
|
|
||||||
import { useAuth } from '../../_providers/Auth'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
type FormData = {
|
|
||||||
email: string
|
|
||||||
name: string
|
|
||||||
password: string
|
|
||||||
passwordConfirm: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AccountForm: React.FC = () => {
|
|
||||||
const [error, setError] = useState('')
|
|
||||||
const [success, setSuccess] = useState('')
|
|
||||||
const { user, setUser } = useAuth()
|
|
||||||
const [changePassword, setChangePassword] = useState(false)
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors, isLoading },
|
|
||||||
reset,
|
|
||||||
watch,
|
|
||||||
} = useForm<FormData>()
|
|
||||||
|
|
||||||
const password = useRef({})
|
|
||||||
password.current = watch('password', '')
|
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
|
||||||
async (data: FormData) => {
|
|
||||||
if (user) {
|
|
||||||
const response = await fetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users/${user.id}`,
|
|
||||||
{
|
|
||||||
// Make sure to include cookies with fetch
|
|
||||||
credentials: 'include',
|
|
||||||
method: 'PATCH',
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const json = await response.json()
|
|
||||||
setUser(json.doc)
|
|
||||||
setSuccess('Successfully updated account.')
|
|
||||||
setError('')
|
|
||||||
setChangePassword(false)
|
|
||||||
reset({
|
|
||||||
email: json.doc.email,
|
|
||||||
name: json.doc.name,
|
|
||||||
password: '',
|
|
||||||
passwordConfirm: '',
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setError('There was a problem updating your account.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[user, setUser, reset],
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user === null) {
|
|
||||||
router.push(`/login?unauthorized=account`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Once user is loaded, reset form to have default values
|
|
||||||
if (user) {
|
|
||||||
reset({
|
|
||||||
email: user.email,
|
|
||||||
password: '',
|
|
||||||
passwordConfirm: '',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [user, router, reset, changePassword])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
|
||||||
<Message error={error} success={success} className={classes.message} />
|
|
||||||
{!changePassword ? (
|
|
||||||
<Fragment>
|
|
||||||
<p>
|
|
||||||
{'To change your password, '}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={classes.changePassword}
|
|
||||||
onClick={() => setChangePassword(!changePassword)}
|
|
||||||
>
|
|
||||||
click here
|
|
||||||
</button>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
<Input
|
|
||||||
name="email"
|
|
||||||
label="Email Address"
|
|
||||||
required
|
|
||||||
register={register}
|
|
||||||
error={errors.email}
|
|
||||||
type="email"
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
) : (
|
|
||||||
<Fragment>
|
|
||||||
<p>
|
|
||||||
{'Change your password below, or '}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={classes.changePassword}
|
|
||||||
onClick={() => setChangePassword(!changePassword)}
|
|
||||||
>
|
|
||||||
cancel
|
|
||||||
</button>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
<Input
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
label="Password"
|
|
||||||
required
|
|
||||||
register={register}
|
|
||||||
error={errors.password}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
name="passwordConfirm"
|
|
||||||
type="password"
|
|
||||||
label="Confirm Password"
|
|
||||||
required
|
|
||||||
register={register}
|
|
||||||
validate={(value) => value === password.current || 'The passwords do not match'}
|
|
||||||
error={errors.passwordConfirm}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className={classes.submit}
|
|
||||||
label={isLoading ? 'Processing' : changePassword ? 'Change password' : 'Update account'}
|
|
||||||
appearance="primary"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
import { Button } from '../_components/Button'
|
|
||||||
import { Gutter } from '../_components/Gutter'
|
|
||||||
import { RenderParams } from '../_components/RenderParams'
|
|
||||||
import { getMeUser } from '../_utilities/getMeUser'
|
|
||||||
import { AccountForm } from './AccountForm'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
export default async function Account() {
|
|
||||||
await getMeUser({
|
|
||||||
nullUserRedirect: `/login?error=${encodeURIComponent(
|
|
||||||
'You must be logged in to access your account.',
|
|
||||||
)}&redirect=${encodeURIComponent('/account')}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Gutter className={classes.account}>
|
|
||||||
<RenderParams className={classes.params} />
|
|
||||||
<h1>Account</h1>
|
|
||||||
<p>
|
|
||||||
{`This is your account dashboard. Here you can update your account information and more. To manage all users, `}
|
|
||||||
<Link href={`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/admin/collections/users`}>
|
|
||||||
login to the admin dashboard
|
|
||||||
</Link>
|
|
||||||
{'.'}
|
|
||||||
</p>
|
|
||||||
<AccountForm />
|
|
||||||
<Button href="/logout" appearance="secondary" label="Log out" />
|
|
||||||
</Gutter>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useCallback, useRef, useState } from 'react'
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
|
||||||
|
|
||||||
import { Button } from '../../_components/Button'
|
|
||||||
import { Input } from '../../_components/Input'
|
|
||||||
import { Message } from '../../_components/Message'
|
|
||||||
import { useAuth } from '../../_providers/Auth'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
type FormData = {
|
|
||||||
email: string
|
|
||||||
password: string
|
|
||||||
passwordConfirm: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CreateAccountForm: React.FC = () => {
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const allParams = searchParams.toString() ? `?${searchParams.toString()}` : ''
|
|
||||||
const { login } = useAuth()
|
|
||||||
const router = useRouter()
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
|
||||||
watch,
|
|
||||||
} = useForm<FormData>()
|
|
||||||
|
|
||||||
const password = useRef({})
|
|
||||||
password.current = watch('password', '')
|
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
|
||||||
async (data: FormData) => {
|
|
||||||
const response = await fetch(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const message = response.statusText || 'There was an error creating the account.'
|
|
||||||
setError(message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const redirect = searchParams.get('redirect')
|
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setLoading(true)
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
try {
|
|
||||||
await login(data)
|
|
||||||
clearTimeout(timer)
|
|
||||||
if (redirect) router.push(redirect as string)
|
|
||||||
else router.push(`/account?success=${encodeURIComponent('Account created successfully')}`)
|
|
||||||
} catch (_) {
|
|
||||||
clearTimeout(timer)
|
|
||||||
setError('There was an error with the credentials provided. Please try again.')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[login, router, searchParams],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
|
||||||
<p>
|
|
||||||
{`This is where new customers can signup and create a new account. To manage all users, `}
|
|
||||||
<Link href={`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/admin/collections/users`}>
|
|
||||||
login to the admin dashboard
|
|
||||||
</Link>
|
|
||||||
{'.'}
|
|
||||||
</p>
|
|
||||||
<Message error={error} className={classes.message} />
|
|
||||||
<Input
|
|
||||||
name="email"
|
|
||||||
label="Email Address"
|
|
||||||
required
|
|
||||||
register={register}
|
|
||||||
error={errors.email}
|
|
||||||
type="email"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
label="Password"
|
|
||||||
required
|
|
||||||
register={register}
|
|
||||||
error={errors.password}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
name="passwordConfirm"
|
|
||||||
type="password"
|
|
||||||
label="Confirm Password"
|
|
||||||
required
|
|
||||||
register={register}
|
|
||||||
validate={(value) => value === password.current || 'The passwords do not match'}
|
|
||||||
error={errors.passwordConfirm}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className={classes.submit}
|
|
||||||
label={loading ? 'Processing' : 'Create Account'}
|
|
||||||
appearance="primary"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
{'Already have an account? '}
|
|
||||||
<Link href={`/login${allParams}`}>Login</Link>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
import { Gutter } from '../_components/Gutter'
|
|
||||||
import { RenderParams } from '../_components/RenderParams'
|
|
||||||
import { getMeUser } from '../_utilities/getMeUser'
|
|
||||||
import { CreateAccountForm } from './CreateAccountForm'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
export default async function CreateAccount() {
|
|
||||||
await getMeUser({
|
|
||||||
validUserRedirect: `/account?message=${encodeURIComponent(
|
|
||||||
'Cannot create a new account while logged in, please log out and try again.',
|
|
||||||
)}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Gutter className={classes.createAccount}>
|
|
||||||
<h1>Create Account</h1>
|
|
||||||
<RenderParams />
|
|
||||||
<CreateAccountForm />
|
|
||||||
</Gutter>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { Header } from './_components/Header'
|
|
||||||
import { AuthProvider } from './_providers/Auth'
|
|
||||||
|
|
||||||
import './_css/app.scss'
|
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: 'Payload Auth + Next.js App Router Example',
|
|
||||||
description: 'An example of how to authenticate with Payload from a Next.js app.',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function RootLayout(props: { children: React.ReactNode }) {
|
|
||||||
const { children } = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<html lang="en">
|
|
||||||
<body>
|
|
||||||
<AuthProvider
|
|
||||||
// To toggle between the REST and GraphQL APIs,
|
|
||||||
// change the `api` prop to either `rest` or `gql`
|
|
||||||
api="rest" // change this to `gql` to use the GraphQL API
|
|
||||||
>
|
|
||||||
<Header />
|
|
||||||
<main>{children}</main>
|
|
||||||
</AuthProvider>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useCallback, useRef } from 'react'
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
|
||||||
|
|
||||||
import { Button } from '../../_components/Button'
|
|
||||||
import { Input } from '../../_components/Input'
|
|
||||||
import { Message } from '../../_components/Message'
|
|
||||||
import { useAuth } from '../../_providers/Auth'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
type FormData = {
|
|
||||||
email: string
|
|
||||||
password: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LoginForm: React.FC = () => {
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const allParams = searchParams.toString() ? `?${searchParams.toString()}` : ''
|
|
||||||
const redirect = useRef(searchParams.get('redirect'))
|
|
||||||
const { login } = useAuth()
|
|
||||||
const router = useRouter()
|
|
||||||
const [error, setError] = React.useState<string | null>(null)
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors, isLoading },
|
|
||||||
} = useForm<FormData>({
|
|
||||||
defaultValues: {
|
|
||||||
email: 'demo@payloadcms.com',
|
|
||||||
password: 'demo',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
|
||||||
async (data: FormData) => {
|
|
||||||
try {
|
|
||||||
await login(data)
|
|
||||||
if (redirect?.current) router.push(redirect.current as string)
|
|
||||||
else router.push('/account')
|
|
||||||
} catch (_) {
|
|
||||||
setError('There was an error with the credentials provided. Please try again.')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[login, router],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
|
||||||
<p>
|
|
||||||
{'To log in, use the email '}
|
|
||||||
<b>demo@payloadcms.com</b>
|
|
||||||
{' with the password '}
|
|
||||||
<b>demo</b>
|
|
||||||
{'. To manage your users, '}
|
|
||||||
<Link href={`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/admin/collections/users`}>
|
|
||||||
login to the admin dashboard
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
<Message error={error} className={classes.message} />
|
|
||||||
<Input
|
|
||||||
name="email"
|
|
||||||
label="Email Address"
|
|
||||||
required
|
|
||||||
register={register}
|
|
||||||
error={errors.email}
|
|
||||||
type="email"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
label="Password"
|
|
||||||
required
|
|
||||||
register={register}
|
|
||||||
error={errors.password}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading}
|
|
||||||
className={classes.submit}
|
|
||||||
label={isLoading ? 'Processing' : 'Login'}
|
|
||||||
appearance="primary"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<Link href={`/create-account${allParams}`}>Create an account</Link>
|
|
||||||
<br />
|
|
||||||
<Link href={`/recover-password${allParams}`}>Recover your password</Link>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
import { Gutter } from '../_components/Gutter'
|
|
||||||
import { RenderParams } from '../_components/RenderParams'
|
|
||||||
import { getMeUser } from '../_utilities/getMeUser'
|
|
||||||
import { LoginForm } from './LoginForm'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
export default async function Login() {
|
|
||||||
await getMeUser({
|
|
||||||
validUserRedirect: `/account?message=${encodeURIComponent('You are already logged in.')}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Gutter className={classes.login}>
|
|
||||||
<RenderParams className={classes.params} />
|
|
||||||
<h1>Log in</h1>
|
|
||||||
<LoginForm />
|
|
||||||
</Gutter>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { Fragment, useEffect, useState } from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
import { useAuth } from '../../_providers/Auth'
|
|
||||||
|
|
||||||
export const LogoutPage: React.FC = (props) => {
|
|
||||||
const { logout } = useAuth()
|
|
||||||
const [success, setSuccess] = useState('')
|
|
||||||
const [error, setError] = useState('')
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const performLogout = async () => {
|
|
||||||
try {
|
|
||||||
await logout()
|
|
||||||
setSuccess('Logged out successfully.')
|
|
||||||
} catch (_) {
|
|
||||||
setError('You are already logged out.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
performLogout()
|
|
||||||
}, [logout])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
{(error || success) && (
|
|
||||||
<div>
|
|
||||||
<h1>{error || success}</h1>
|
|
||||||
<p>
|
|
||||||
{'What would you like to do next? '}
|
|
||||||
<Link href="/">Click here</Link>
|
|
||||||
{` to go to the home page. To log back in, `}
|
|
||||||
<Link href="/login">click here</Link>
|
|
||||||
{'.'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
import { Gutter } from '../_components/Gutter'
|
|
||||||
import { LogoutPage } from './LogoutPage'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
export default async function Logout() {
|
|
||||||
return (
|
|
||||||
<Gutter className={classes.logout}>
|
|
||||||
<LogoutPage />
|
|
||||||
</Gutter>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
import { Gutter } from './_components/Gutter'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<Gutter>
|
|
||||||
<h1>Payload Auth Example</h1>
|
|
||||||
<p>
|
|
||||||
{'This is a '}
|
|
||||||
<Link href="https://payloadcms.com" target="_blank" rel="noopener noreferrer">
|
|
||||||
Payload
|
|
||||||
</Link>
|
|
||||||
{' + '}
|
|
||||||
<Link href="https://nextjs.org" target="_blank" rel="noopener noreferrer">
|
|
||||||
Next.js
|
|
||||||
</Link>
|
|
||||||
{' app using the '}
|
|
||||||
<Link href="https://nextjs.org/docs/app" target="_blank" rel="noopener noreferrer">
|
|
||||||
App Router
|
|
||||||
</Link>
|
|
||||||
{' made explicitly for the '}
|
|
||||||
<Link href="https://github.com/payloadcms/payload/tree/main/examples/auth">
|
|
||||||
Payload Auth Example
|
|
||||||
</Link>
|
|
||||||
{". This example demonstrates how to implement Payload's "}
|
|
||||||
<Link href="https://payloadcms.com/docs/authentication/overview">Authentication</Link>
|
|
||||||
{
|
|
||||||
' strategies through http using the REST and GraphQL APIs. To toggle between these two APIs, see `_layout.tsx`.'
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{'Visit the '}
|
|
||||||
<Link href="/login">login page</Link>
|
|
||||||
{' to start the authentication flow. Once logged in, you will be redirected to the '}
|
|
||||||
<Link href="/account">account page</Link>
|
|
||||||
{` which is restricted to users only. To manage all users, `}
|
|
||||||
<Link href={`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/admin/collections/users`}>
|
|
||||||
login to the admin dashboard
|
|
||||||
</Link>
|
|
||||||
{'.'}
|
|
||||||
</p>
|
|
||||||
</Gutter>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
/**
|
|
||||||
* This file was automatically generated by Payload.
|
|
||||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
|
||||||
* and re-run `payload generate:types` to regenerate this file.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface Config {
|
|
||||||
collections: {
|
|
||||||
users: User
|
|
||||||
}
|
|
||||||
globals: {}
|
|
||||||
}
|
|
||||||
export interface User {
|
|
||||||
id: string
|
|
||||||
firstName?: string
|
|
||||||
lastName?: string
|
|
||||||
roles?: ('admin' | 'user')[]
|
|
||||||
updatedAt: string
|
|
||||||
createdAt: string
|
|
||||||
email: string
|
|
||||||
resetPasswordToken?: string
|
|
||||||
resetPasswordExpiration?: string
|
|
||||||
salt?: string
|
|
||||||
hash?: string
|
|
||||||
loginAttempts?: number
|
|
||||||
lockUntil?: string
|
|
||||||
password?: string
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { Fragment, useCallback, useState } from 'react'
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
import { Button } from '../../_components/Button'
|
|
||||||
import { Input } from '../../_components/Input'
|
|
||||||
import { Message } from '../../_components/Message'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
type FormData = {
|
|
||||||
email: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RecoverPasswordForm: React.FC = () => {
|
|
||||||
const [error, setError] = useState('')
|
|
||||||
const [success, setSuccess] = useState(false)
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<FormData>()
|
|
||||||
|
|
||||||
const onSubmit = useCallback(async (data: FormData) => {
|
|
||||||
const response = await fetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users/forgot-password`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
setSuccess(true)
|
|
||||||
setError('')
|
|
||||||
} else {
|
|
||||||
setError(
|
|
||||||
'There was a problem while attempting to send you a password reset email. Please try again.',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
{!success && (
|
|
||||||
<React.Fragment>
|
|
||||||
<h1>Recover Password</h1>
|
|
||||||
<div className={classes.formWrapper}>
|
|
||||||
<p>
|
|
||||||
{`Please enter your email below. You will receive an email message with instructions on
|
|
||||||
how to reset your password. To manage all of your users, `}
|
|
||||||
<Link href={`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/admin/collections/users`}>
|
|
||||||
login to the admin dashboard
|
|
||||||
</Link>
|
|
||||||
{'.'}
|
|
||||||
</p>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
|
||||||
<Message error={error} className={classes.message} />
|
|
||||||
<Input
|
|
||||||
name="email"
|
|
||||||
label="Email Address"
|
|
||||||
required
|
|
||||||
register={register}
|
|
||||||
error={errors.email}
|
|
||||||
type="email"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className={classes.submit}
|
|
||||||
label="Recover Password"
|
|
||||||
appearance="primary"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</React.Fragment>
|
|
||||||
)}
|
|
||||||
{success && (
|
|
||||||
<React.Fragment>
|
|
||||||
<h1>Request submitted</h1>
|
|
||||||
<p>Check your email for a link that will allow you to securely reset your password.</p>
|
|
||||||
</React.Fragment>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
import { Gutter } from '../_components/Gutter'
|
|
||||||
import { RecoverPasswordForm } from './RecoverPasswordForm'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
export default async function RecoverPassword() {
|
|
||||||
return (
|
|
||||||
<Gutter className={classes.recoverPassword}>
|
|
||||||
<RecoverPasswordForm />
|
|
||||||
</Gutter>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
|
||||||
|
|
||||||
import { Button } from '../../_components/Button'
|
|
||||||
import { Input } from '../../_components/Input'
|
|
||||||
import { Message } from '../../_components/Message'
|
|
||||||
import { useAuth } from '../../_providers/Auth'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
type FormData = {
|
|
||||||
password: string
|
|
||||||
token: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ResetPasswordForm: React.FC = () => {
|
|
||||||
const [error, setError] = useState('')
|
|
||||||
const { login } = useAuth()
|
|
||||||
const router = useRouter()
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const token = searchParams.get('token')
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
|
||||||
reset,
|
|
||||||
} = useForm<FormData>()
|
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
|
||||||
async (data: FormData) => {
|
|
||||||
const response = await fetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users/reset-password`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const json = await response.json()
|
|
||||||
|
|
||||||
// Automatically log the user in after they successfully reset password
|
|
||||||
await login({ email: json.user.email, password: data.password })
|
|
||||||
|
|
||||||
// Redirect them to `/account` with success message in URL
|
|
||||||
router.push('/account?success=Password reset successfully.')
|
|
||||||
} else {
|
|
||||||
setError('There was a problem while resetting your password. Please try again later.')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[router, login],
|
|
||||||
)
|
|
||||||
|
|
||||||
// when Next.js populates token within router,
|
|
||||||
// reset form with new token value
|
|
||||||
useEffect(() => {
|
|
||||||
reset({ token: token || undefined })
|
|
||||||
}, [reset, token])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
|
||||||
<Message error={error} className={classes.message} />
|
|
||||||
<Input
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
label="New Password"
|
|
||||||
required
|
|
||||||
register={register}
|
|
||||||
error={errors.password}
|
|
||||||
/>
|
|
||||||
<input type="hidden" {...register('token')} />
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className={classes.submit}
|
|
||||||
label="Reset Password"
|
|
||||||
appearance="primary"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
import { Gutter } from '../_components/Gutter'
|
|
||||||
import { ResetPasswordForm } from './ResetPasswordForm'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
export default async function ResetPassword() {
|
|
||||||
return (
|
|
||||||
<Gutter className={classes.resetPassword}>
|
|
||||||
<h1>Reset Password</h1>
|
|
||||||
<p>Please enter a new password below.</p>
|
|
||||||
<ResetPasswordForm />
|
|
||||||
</Gutter>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
|
||||||
const nextConfig = {}
|
|
||||||
|
|
||||||
module.exports = nextConfig
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "payload-auth-next-app",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"dev": "next dev -p 3001",
|
|
||||||
"build": "next build",
|
|
||||||
"start": "next start -p 3001",
|
|
||||||
"lint": "next lint"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"escape-html": "^1.0.3",
|
|
||||||
"next": "^13.5.1",
|
|
||||||
"react": "18.2.0",
|
|
||||||
"react-dom": "18.2.0",
|
|
||||||
"react-hook-form": "^7.45.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@next/eslint-plugin-next": "^13.4.8",
|
|
||||||
"@payloadcms/eslint-config": "^0.0.2",
|
|
||||||
"@types/escape-html": "^1.0.2",
|
|
||||||
"@types/node": "18.11.3",
|
|
||||||
"@types/react": "^18.2.14",
|
|
||||||
"@types/react-dom": "^18.2.6",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
|
||||||
"@typescript-eslint/parser": "^5.51.0",
|
|
||||||
"eslint": "8.41.0",
|
|
||||||
"eslint-config-next": "13.4.3",
|
|
||||||
"eslint-config-prettier": "^8.5.0",
|
|
||||||
"eslint-plugin-filenames": "^1.3.2",
|
|
||||||
"eslint-plugin-import": "2.25.4",
|
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
|
||||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
|
||||||
"prettier": "^2.7.1",
|
|
||||||
"sass": "^1.62.1",
|
|
||||||
"slate": "^0.82.0",
|
|
||||||
"typescript": "^4.8.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3766
examples/auth/next-app/pnpm-lock.yaml
generated
3766
examples/auth/next-app/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,15 +0,0 @@
|
|||||||
<svg width="260" height="260" viewBox="0 0 260 260" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<style>
|
|
||||||
path {
|
|
||||||
fill: #0F0F0F;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
path {
|
|
||||||
fill: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<path d="M120.59 8.5824L231.788 75.6142V202.829L148.039 251.418V124.203L36.7866 57.2249L120.59 8.5824Z" />
|
|
||||||
<path d="M112.123 244.353V145.073L28.2114 193.769L112.123 244.353Z" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 437 B |
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2022",
|
|
||||||
"lib": [
|
|
||||||
"DOM",
|
|
||||||
"DOM.Iterable",
|
|
||||||
"ES2022"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"incremental": true,
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "next"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
||||||
"exclude": ["node_modules"]
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
NEXT_PUBLIC_PAYLOAD_URL=http://localhost:3000
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
extends: ['plugin:@next/next/recommended', '@payloadcms'],
|
|
||||||
}
|
|
||||||
6
examples/auth/next-pages/.gitignore
vendored
6
examples/auth/next-pages/.gitignore
vendored
@@ -1,6 +0,0 @@
|
|||||||
.next
|
|
||||||
dist
|
|
||||||
build
|
|
||||||
node_modules
|
|
||||||
.env
|
|
||||||
package-lock.json
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# Payload Auth Example Front-End
|
|
||||||
|
|
||||||
This is a [Next.js](https://nextjs.org) [Pages Router](https://nextjs.org/docs/pages) front-end made explicitly for the [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth). This example demonstrates how to authenticate your Next.js app using [Payload Authentication](https://payloadcms.com/docs/authentication/overview).
|
|
||||||
|
|
||||||
> This example uses the Pages Router, the legacy API of Next.js. If your app is using the latest [App Router](https://nextjs.org/docs/app), check out the official [App Router Example](https://github.com/payloadcms/payload/tree/main/examples/auth/next-app).
|
|
||||||
|
|
||||||
**IMPORTANT—This application runs on a different server as Payload and establishes a connection from another domain or port over HTTP.** For an integrated setup that runs on a single server and uses the [Local API](https://payloadcms.com/docs/local-api/overview#local-api), check out [how to serve Payload alongside Next.js](https://github.com/payloadcms/payload/tree/main/examples/auth/payload). To learn more about this, check out [how Payload can be used in its various headless capacities](https://payloadcms.com/blog/the-ultimate-guide-to-using-nextjs-with-payload).
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Payload
|
|
||||||
|
|
||||||
First you'll need a running Payload app. There is one made explicitly for this example and [can be found here](https://github.com/payloadcms/payload/tree/main/examples/auth/payload). If you have not done so already, clone it down and follow the setup instructions there. This will provide all the necessary APIs that your Next.js app requires for authentication.
|
|
||||||
|
|
||||||
### Next.js
|
|
||||||
|
|
||||||
1. Clone this repo
|
|
||||||
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
|
|
||||||
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
|
|
||||||
3. `cp .env.example .env` to copy the example environment variables
|
|
||||||
4. `pnpm i`, `yarn dev`, or `npm run dev` to start the server
|
|
||||||
5. `open http://localhost:3001` to see the result
|
|
||||||
|
|
||||||
Once running, a user is automatically seeded in your local environment with some basic instructions. See the [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth) for full details.
|
|
||||||
|
|
||||||
## Learn More
|
|
||||||
|
|
||||||
To learn more about Payload and Next.js, take a look at the following resources:
|
|
||||||
|
|
||||||
- [Payload Documentation](https://payloadcms.com/docs) - learn about Payload features and API.
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
||||||
|
|
||||||
You can check out [the Payload GitHub repository](https://github.com/payloadcms/payload) as well as [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new) from the creators of Next.js. You could also combine this app into a [single Express server](https://github.com/payloadcms/payload/tree/main/examples/custom-server) and deploy in to [Payload Cloud](https://payloadcms.com/new/import).
|
|
||||||
|
|
||||||
Check out our [Payload deployment documentation](https://payloadcms.com/docs/production/deployment) or the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
|
||||||
|
|
||||||
## Questions
|
|
||||||
|
|
||||||
If you have any issues or questions, reach out to us on [Discord](https://discord.com/invite/payload) or start a [GitHub discussion](https://github.com/payloadcms/payload/discussions).
|
|
||||||
5
examples/auth/next-pages/next-env.d.ts
vendored
5
examples/auth/next-pages/next-env.d.ts
vendored
@@ -1,5 +0,0 @@
|
|||||||
/// <reference types="next" />
|
|
||||||
/// <reference types="next/image-types/global" />
|
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
|
||||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
|
||||||
const nextConfig = {
|
|
||||||
reactStrictMode: true,
|
|
||||||
swcMinify: true,
|
|
||||||
images: {
|
|
||||||
domains: ['localhost', process.env.NEXT_PUBLIC_PAYLOAD_URL || ''].filter(Boolean),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = nextConfig
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "payload-auth-next-pages",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "next dev -p 3001",
|
|
||||||
"build": "next build",
|
|
||||||
"start": "next start",
|
|
||||||
"lint": "next lint",
|
|
||||||
"prettier:fix": "prettier --write ."
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"escape-html": "^1.0.3",
|
|
||||||
"next": "^13.5.1",
|
|
||||||
"react": "18.2.0",
|
|
||||||
"react-dom": "18.2.0",
|
|
||||||
"react-hook-form": "^7.34.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@next/eslint-plugin-next": "^13.1.6",
|
|
||||||
"@payloadcms/eslint-config": "^0.0.2",
|
|
||||||
"@types/node": "18.11.3",
|
|
||||||
"@types/react": "^18.2.14",
|
|
||||||
"@types/react-dom": "^18.2.6",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
|
||||||
"@typescript-eslint/parser": "^5.51.0",
|
|
||||||
"eslint": "8.25.0",
|
|
||||||
"eslint-config-prettier": "^8.5.0",
|
|
||||||
"eslint-plugin-filenames": "^1.3.2",
|
|
||||||
"eslint-plugin-import": "2.25.4",
|
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
|
||||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
|
||||||
"prettier": "^2.7.1",
|
|
||||||
"sass": "^1.69.7",
|
|
||||||
"slate": "^0.82.0",
|
|
||||||
"typescript": "4.8.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3144
examples/auth/next-pages/pnpm-lock.yaml
generated
3144
examples/auth/next-pages/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,15 +0,0 @@
|
|||||||
<svg width="260" height="260" viewBox="0 0 260 260" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<style>
|
|
||||||
path {
|
|
||||||
fill: #0F0F0F;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
path {
|
|
||||||
fill: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<path d="M120.59 8.5824L231.788 75.6142V202.829L148.039 251.418V124.203L36.7866 57.2249L120.59 8.5824Z" />
|
|
||||||
<path d="M112.123 244.353V145.073L28.2114 193.769L112.123 244.353Z" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 437 B |
@@ -1,40 +0,0 @@
|
|||||||
@import '../../css/type.scss';
|
|
||||||
|
|
||||||
.button {
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-flex;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: transparent;
|
|
||||||
text-decoration: none;
|
|
||||||
display: inline-flex;
|
|
||||||
padding: 12px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-around;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
@extend %label;
|
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.appearance--primary {
|
|
||||||
background-color: var(--theme-elevation-1000);
|
|
||||||
color: var(--theme-elevation-0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.appearance--secondary {
|
|
||||||
background-color: transparent;
|
|
||||||
box-shadow: inset 0 0 0 1px var(--theme-elevation-1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
.appearance--default {
|
|
||||||
padding: 0;
|
|
||||||
color: var(--theme-text);
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import React, { ElementType } from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
export type Props = {
|
|
||||||
label?: string
|
|
||||||
appearance?: 'default' | 'primary' | 'secondary'
|
|
||||||
el?: 'button' | 'link' | 'a'
|
|
||||||
onClick?: () => void
|
|
||||||
href?: string
|
|
||||||
newTab?: boolean
|
|
||||||
className?: string
|
|
||||||
type?: 'submit' | 'button'
|
|
||||||
disabled?: boolean
|
|
||||||
invert?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Button: React.FC<Props> = ({
|
|
||||||
el: elFromProps = 'link',
|
|
||||||
label,
|
|
||||||
newTab,
|
|
||||||
href,
|
|
||||||
appearance,
|
|
||||||
className: classNameFromProps,
|
|
||||||
onClick,
|
|
||||||
type = 'button',
|
|
||||||
disabled,
|
|
||||||
invert,
|
|
||||||
}) => {
|
|
||||||
let el = elFromProps
|
|
||||||
const newTabProps = newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {}
|
|
||||||
|
|
||||||
const className = [
|
|
||||||
classes.button,
|
|
||||||
classNameFromProps,
|
|
||||||
classes[`appearance--${appearance}`],
|
|
||||||
invert && classes[`${appearance}--invert`],
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ')
|
|
||||||
|
|
||||||
const content = (
|
|
||||||
<div className={classes.content}>
|
|
||||||
<span className={classes.label}>{label}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (onClick || type === 'submit') el = 'button'
|
|
||||||
|
|
||||||
if (el === 'link') {
|
|
||||||
return (
|
|
||||||
<Link href={href || ''} className={className} {...newTabProps} onClick={onClick}>
|
|
||||||
{content}
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Element: ElementType = el
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Element
|
|
||||||
href={href}
|
|
||||||
className={className}
|
|
||||||
type={type}
|
|
||||||
{...newTabProps}
|
|
||||||
onClick={onClick}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</Element>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
.gutter {
|
|
||||||
max-width: 1920px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gutterLeft {
|
|
||||||
padding-left: var(--gutter-h);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gutterRight {
|
|
||||||
padding-right: var(--gutter-h);
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import React, { forwardRef, Ref } from 'react'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
left?: boolean
|
|
||||||
right?: boolean
|
|
||||||
className?: string
|
|
||||||
children: React.ReactNode
|
|
||||||
ref?: Ref<HTMLDivElement>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Gutter: React.FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
|
||||||
const { left = true, right = true, className, children } = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={[
|
|
||||||
classes.gutter,
|
|
||||||
left && classes.gutterLeft,
|
|
||||||
right && classes.gutterRight,
|
|
||||||
className,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ')}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
Gutter.displayName = 'Gutter'
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
@use '../../../css/queries.scss' as *;
|
|
||||||
|
|
||||||
.nav {
|
|
||||||
display: flex;
|
|
||||||
gap: calc(var(--base) / 4) var(--base);
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 100ms linear;
|
|
||||||
visibility: visible;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide {
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
import { useAuth } from '../../../providers/Auth'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
export const HeaderNav: React.FC = () => {
|
|
||||||
const { user } = useAuth()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav
|
|
||||||
className={[
|
|
||||||
classes.nav,
|
|
||||||
// fade the nav in on user load to avoid flash of content and layout shift
|
|
||||||
// Vercel also does this in their own website header, see https://vercel.com
|
|
||||||
user === undefined && classes.hide,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ')}
|
|
||||||
>
|
|
||||||
{user && (
|
|
||||||
<React.Fragment>
|
|
||||||
<Link href="/account">Account</Link>
|
|
||||||
<Link href="/logout">Logout</Link>
|
|
||||||
</React.Fragment>
|
|
||||||
)}
|
|
||||||
{!user && (
|
|
||||||
<React.Fragment>
|
|
||||||
<Link href="/login">Login</Link>
|
|
||||||
<Link href="/create-account">Create Account</Link>
|
|
||||||
</React.Fragment>
|
|
||||||
)}
|
|
||||||
</nav>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
@use '../../css/queries.scss' as *;
|
|
||||||
|
|
||||||
.header {
|
|
||||||
padding: var(--base) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrap {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: calc(var(--base) / 2) var(--base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
width: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global([data-theme='light']) {
|
|
||||||
.logo {
|
|
||||||
filter: invert(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import Image from 'next/image'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
import { Gutter } from '../Gutter'
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
import { HeaderNav } from './Nav'
|
|
||||||
|
|
||||||
export const Header: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<header className={classes.header}>
|
|
||||||
<Gutter className={classes.wrap}>
|
|
||||||
<Link className={classes.logo} href="/">
|
|
||||||
<picture>
|
|
||||||
<source
|
|
||||||
media="(prefers-color-scheme: dark)"
|
|
||||||
srcSet="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-logo-light.svg"
|
|
||||||
/>
|
|
||||||
<Image
|
|
||||||
alt="Payload Logo"
|
|
||||||
height={30}
|
|
||||||
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-logo-dark.svg"
|
|
||||||
width={150}
|
|
||||||
/>
|
|
||||||
</picture>
|
|
||||||
</Link>
|
|
||||||
<HeaderNav />
|
|
||||||
</Gutter>
|
|
||||||
</header>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Header
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
@import '../../css/common';
|
|
||||||
|
|
||||||
.inputWrap {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
width: 100%;
|
|
||||||
font-family: system-ui;
|
|
||||||
border-radius: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
background-color: var(--theme-elevation-100);
|
|
||||||
color: var(--theme-elevation-1000);
|
|
||||||
height: calc(var(--base) * 2);
|
|
||||||
line-height: calc(var(--base) * 2);
|
|
||||||
padding: 0 calc(var(--base) / 2);
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:-webkit-autofill,
|
|
||||||
&:-webkit-autofill:hover,
|
|
||||||
&:-webkit-autofill:focus {
|
|
||||||
-webkit-text-fill-color: var(--theme-text);
|
|
||||||
-webkit-box-shadow: 0 0 0px 1000px var(--theme-elevation-150) inset;
|
|
||||||
transition: background-color 5000s ease-in-out 0s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.input {
|
|
||||||
background-color: var(--theme-elevation-150);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
background-color: var(--theme-error-150);
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
margin-bottom: 0;
|
|
||||||
display: block;
|
|
||||||
line-height: 1;
|
|
||||||
margin-bottom: calc(var(--base) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.errorMessage {
|
|
||||||
font-size: small;
|
|
||||||
line-height: 1.25;
|
|
||||||
margin-top: 4px;
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { FieldValues, UseFormRegister, Validate } from 'react-hook-form'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
name: string
|
|
||||||
label: string
|
|
||||||
register: UseFormRegister<FieldValues & any>
|
|
||||||
required?: boolean
|
|
||||||
error: any
|
|
||||||
type?: 'text' | 'number' | 'password' | 'email'
|
|
||||||
validate?: (value: string) => boolean | string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Input: React.FC<Props> = ({
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
required,
|
|
||||||
register,
|
|
||||||
error,
|
|
||||||
type = 'text',
|
|
||||||
validate,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className={classes.inputWrap}>
|
|
||||||
<label htmlFor="name" className={classes.label}>
|
|
||||||
{`${label} ${required ? '*' : ''}`}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
className={[classes.input, error && classes.error].filter(Boolean).join(' ')}
|
|
||||||
{...{ type }}
|
|
||||||
{...register(name, {
|
|
||||||
required,
|
|
||||||
validate,
|
|
||||||
...(type === 'email'
|
|
||||||
? {
|
|
||||||
pattern: {
|
|
||||||
value: /\S+@\S+\.\S+/,
|
|
||||||
message: 'Please enter a valid email',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
{error && (
|
|
||||||
<div className={classes.errorMessage}>
|
|
||||||
{!error?.message && error?.type === 'required'
|
|
||||||
? 'This field is required'
|
|
||||||
: error?.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
@import '../../css/common';
|
|
||||||
|
|
||||||
.message {
|
|
||||||
padding: calc(var(--base) / 2) calc(var(--base) / 2);
|
|
||||||
line-height: 1.25;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.default {
|
|
||||||
background-color: var(--theme-elevation-100);
|
|
||||||
color: var(--theme-elevation-1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning {
|
|
||||||
background-color: var(--theme-warning-500);
|
|
||||||
color: var(--theme-warning-900);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
background-color: var(--theme-error-500);
|
|
||||||
color: var(--theme-error-900);
|
|
||||||
}
|
|
||||||
|
|
||||||
.success {
|
|
||||||
background-color: var(--theme-success-500);
|
|
||||||
color: var(--theme-success-900);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.default {
|
|
||||||
background-color: var(--theme-elevation-900);
|
|
||||||
color: var(--theme-elevation-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.warning {
|
|
||||||
color: var(--theme-warning-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: var(--theme-error-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.success {
|
|
||||||
color: var(--theme-success-100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
export const Message: React.FC<{
|
|
||||||
message?: React.ReactNode
|
|
||||||
error?: React.ReactNode
|
|
||||||
success?: React.ReactNode
|
|
||||||
warning?: React.ReactNode
|
|
||||||
className?: string
|
|
||||||
}> = ({ message, error, success, warning, className }) => {
|
|
||||||
const messageToRender = message || error || success || warning
|
|
||||||
|
|
||||||
if (messageToRender) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={[
|
|
||||||
classes.message,
|
|
||||||
className,
|
|
||||||
error && classes.error,
|
|
||||||
success && classes.success,
|
|
||||||
warning && classes.warning,
|
|
||||||
!error && !success && !warning && classes.default,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ')}
|
|
||||||
>
|
|
||||||
{messageToRender}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { useRouter } from 'next/router'
|
|
||||||
|
|
||||||
import { Message } from '../Message'
|
|
||||||
|
|
||||||
export const RenderParams: React.FC<{
|
|
||||||
params?: string[]
|
|
||||||
message?: string
|
|
||||||
className?: string
|
|
||||||
}> = ({ params = ['error', 'message', 'success'], message, className }) => {
|
|
||||||
const router = useRouter()
|
|
||||||
const searchParams = new URLSearchParams(router.query as any)
|
|
||||||
const paramValues = params.map((param) => searchParams.get(param)).filter(Boolean)
|
|
||||||
|
|
||||||
if (paramValues.length) {
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
{paramValues.map((paramValue) => (
|
|
||||||
<Message key={paramValue} message={(message || 'PARAM')?.replace('PARAM', paramValue)} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
.richText {
|
|
||||||
:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
import serialize from './serialize'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
const RichText: React.FC<{ className?: string; content: any }> = ({ className, content }) => {
|
|
||||||
if (!content) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={[classes.richText, className].filter(Boolean).join(' ')}>
|
|
||||||
{serialize(content)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RichText
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import React, { Fragment } from 'react'
|
|
||||||
import escapeHTML from 'escape-html'
|
|
||||||
import { Text } from 'slate'
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-use-before-define
|
|
||||||
type Children = Leaf[]
|
|
||||||
|
|
||||||
type Leaf = {
|
|
||||||
type: string
|
|
||||||
value?: {
|
|
||||||
url: string
|
|
||||||
alt: string
|
|
||||||
}
|
|
||||||
children?: Children
|
|
||||||
url?: string
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
const serialize = (children: Children): React.ReactElement[] =>
|
|
||||||
children.map((node, i) => {
|
|
||||||
if (Text.isText(node)) {
|
|
||||||
let text = <span dangerouslySetInnerHTML={{ __html: escapeHTML(node.text) }} />
|
|
||||||
|
|
||||||
if (node.bold) {
|
|
||||||
text = <strong key={i}>{text}</strong>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.code) {
|
|
||||||
text = <code key={i}>{text}</code>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.italic) {
|
|
||||||
text = <em key={i}>{text}</em>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.underline) {
|
|
||||||
text = (
|
|
||||||
<span style={{ textDecoration: 'underline' }} key={i}>
|
|
||||||
{text}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.strikethrough) {
|
|
||||||
text = (
|
|
||||||
<span style={{ textDecoration: 'line-through' }} key={i}>
|
|
||||||
{text}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Fragment key={i}>{text}</Fragment>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!node) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (node.type) {
|
|
||||||
case 'h1':
|
|
||||||
return <h1 key={i}>{serialize(node.children)}</h1>
|
|
||||||
case 'h2':
|
|
||||||
return <h2 key={i}>{serialize(node.children)}</h2>
|
|
||||||
case 'h3':
|
|
||||||
return <h3 key={i}>{serialize(node.children)}</h3>
|
|
||||||
case 'h4':
|
|
||||||
return <h4 key={i}>{serialize(node.children)}</h4>
|
|
||||||
case 'h5':
|
|
||||||
return <h5 key={i}>{serialize(node.children)}</h5>
|
|
||||||
case 'h6':
|
|
||||||
return <h6 key={i}>{serialize(node.children)}</h6>
|
|
||||||
case 'blockquote':
|
|
||||||
return <blockquote key={i}>{serialize(node.children)}</blockquote>
|
|
||||||
case 'ul':
|
|
||||||
return <ul key={i}>{serialize(node.children)}</ul>
|
|
||||||
case 'ol':
|
|
||||||
return <ol key={i}>{serialize(node.children)}</ol>
|
|
||||||
case 'li':
|
|
||||||
return <li key={i}>{serialize(node.children)}</li>
|
|
||||||
case 'link':
|
|
||||||
return (
|
|
||||||
<a href={escapeHTML(node.url)} key={i}>
|
|
||||||
{serialize(node.children)}
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return <p key={i}>{serialize(node.children)}</p>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default serialize
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
@use './queries.scss' as *;
|
|
||||||
@use './colors.scss' as *;
|
|
||||||
@use './type.scss' as *;
|
|
||||||
@import './theme.scss';
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--base: 24px;
|
|
||||||
--font-body: system-ui;
|
|
||||||
--font-mono: 'Roboto Mono', monospace;
|
|
||||||
|
|
||||||
--gutter-h: 180px;
|
|
||||||
--block-padding: 120px;
|
|
||||||
|
|
||||||
@include large-break {
|
|
||||||
--gutter-h: 144px;
|
|
||||||
--block-padding: 96px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include mid-break {
|
|
||||||
--gutter-h: 24px;
|
|
||||||
--block-padding: 60px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
@extend %body;
|
|
||||||
background: var(--theme-bg);
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body,
|
|
||||||
#app {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: var(--font-body);
|
|
||||||
margin: 0;
|
|
||||||
color: var(--theme-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
::selection {
|
|
||||||
background: var(--theme-success-500);
|
|
||||||
color: var(--color-base-800);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-moz-selection {
|
|
||||||
background: var(--theme-success-500);
|
|
||||||
color: var(--color-base-800);
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
@extend %h1;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
@extend %h2;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
@extend %h3;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
@extend %h4;
|
|
||||||
}
|
|
||||||
|
|
||||||
h5 {
|
|
||||||
@extend %h5;
|
|
||||||
}
|
|
||||||
|
|
||||||
h6 {
|
|
||||||
@extend %h6;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: var(--base) 0;
|
|
||||||
|
|
||||||
@include mid-break {
|
|
||||||
margin: calc(var(--base) * 0.75) 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul,
|
|
||||||
ol {
|
|
||||||
padding-left: var(--base);
|
|
||||||
margin: 0 0 var(--base);
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: currentColor;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
opacity: 0.8;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
opacity: 0.7;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
:root {
|
|
||||||
--color-base-0: rgb(255, 255, 255);
|
|
||||||
--color-base-50: rgb(245, 245, 245);
|
|
||||||
--color-base-100: rgb(235, 235, 235);
|
|
||||||
--color-base-150: rgb(221, 221, 221);
|
|
||||||
--color-base-200: rgb(208, 208, 208);
|
|
||||||
--color-base-250: rgb(195, 195, 195);
|
|
||||||
--color-base-300: rgb(181, 181, 181);
|
|
||||||
--color-base-350: rgb(168, 168, 168);
|
|
||||||
--color-base-400: rgb(154, 154, 154);
|
|
||||||
--color-base-450: rgb(141, 141, 141);
|
|
||||||
--color-base-500: rgb(128, 128, 128);
|
|
||||||
--color-base-550: rgb(114, 114, 114);
|
|
||||||
--color-base-600: rgb(101, 101, 101);
|
|
||||||
--color-base-650: rgb(87, 87, 87);
|
|
||||||
--color-base-700: rgb(74, 74, 74);
|
|
||||||
--color-base-750: rgb(60, 60, 60);
|
|
||||||
--color-base-800: rgb(47, 47, 47);
|
|
||||||
--color-base-850: rgb(34, 34, 34);
|
|
||||||
--color-base-900: rgb(20, 20, 20);
|
|
||||||
--color-base-950: rgb(7, 7, 7);
|
|
||||||
--color-base-1000: rgb(0, 0, 0);
|
|
||||||
|
|
||||||
--color-success-50: rgb(237, 245, 249);
|
|
||||||
--color-success-100: rgb(218, 237, 248);
|
|
||||||
--color-success-150: rgb(188, 225, 248);
|
|
||||||
--color-success-200: rgb(156, 216, 253);
|
|
||||||
--color-success-250: rgb(125, 204, 248);
|
|
||||||
--color-success-300: rgb(97, 190, 241);
|
|
||||||
--color-success-350: rgb(65, 178, 236);
|
|
||||||
--color-success-400: rgb(36, 164, 223);
|
|
||||||
--color-success-450: rgb(18, 148, 204);
|
|
||||||
--color-success-500: rgb(21, 135, 186);
|
|
||||||
--color-success-550: rgb(12, 121, 168);
|
|
||||||
--color-success-600: rgb(11, 110, 153);
|
|
||||||
--color-success-650: rgb(11, 97, 135);
|
|
||||||
--color-success-700: rgb(17, 88, 121);
|
|
||||||
--color-success-750: rgb(17, 76, 105);
|
|
||||||
--color-success-800: rgb(18, 66, 90);
|
|
||||||
--color-success-850: rgb(18, 56, 76);
|
|
||||||
--color-success-900: rgb(19, 44, 58);
|
|
||||||
--color-success-950: rgb(22, 33, 39);
|
|
||||||
|
|
||||||
--color-error-50: rgb(250, 241, 240);
|
|
||||||
--color-error-100: rgb(252, 229, 227);
|
|
||||||
--color-error-150: rgb(247, 208, 204);
|
|
||||||
--color-error-200: rgb(254, 193, 188);
|
|
||||||
--color-error-250: rgb(253, 177, 170);
|
|
||||||
--color-error-300: rgb(253, 154, 146);
|
|
||||||
--color-error-350: rgb(253, 131, 123);
|
|
||||||
--color-error-400: rgb(246, 109, 103);
|
|
||||||
--color-error-450: rgb(234, 90, 86);
|
|
||||||
--color-error-500: rgb(218, 75, 72);
|
|
||||||
--color-error-550: rgb(200, 62, 61);
|
|
||||||
--color-error-600: rgb(182, 54, 54);
|
|
||||||
--color-error-650: rgb(161, 47, 47);
|
|
||||||
--color-error-700: rgb(144, 44, 43);
|
|
||||||
--color-error-750: rgb(123, 41, 39);
|
|
||||||
--color-error-800: rgb(105, 39, 37);
|
|
||||||
--color-error-850: rgb(86, 36, 33);
|
|
||||||
--color-error-900: rgb(64, 32, 29);
|
|
||||||
--color-error-950: rgb(44, 26, 24);
|
|
||||||
|
|
||||||
--color-warning-50: rgb(249, 242, 237);
|
|
||||||
--color-warning-100: rgb(248, 232, 219);
|
|
||||||
--color-warning-150: rgb(243, 212, 186);
|
|
||||||
--color-warning-200: rgb(243, 200, 162);
|
|
||||||
--color-warning-250: rgb(240, 185, 136);
|
|
||||||
--color-warning-300: rgb(238, 166, 98);
|
|
||||||
--color-warning-350: rgb(234, 148, 58);
|
|
||||||
--color-warning-400: rgb(223, 132, 17);
|
|
||||||
--color-warning-450: rgb(204, 120, 15);
|
|
||||||
--color-warning-500: rgb(185, 108, 13);
|
|
||||||
--color-warning-550: rgb(167, 97, 10);
|
|
||||||
--color-warning-600: rgb(150, 87, 11);
|
|
||||||
--color-warning-650: rgb(134, 78, 11);
|
|
||||||
--color-warning-700: rgb(120, 70, 13);
|
|
||||||
--color-warning-750: rgb(105, 61, 13);
|
|
||||||
--color-warning-800: rgb(90, 55, 19);
|
|
||||||
--color-warning-850: rgb(73, 47, 21);
|
|
||||||
--color-warning-900: rgb(56, 38, 20);
|
|
||||||
--color-warning-950: rgb(38, 29, 21);
|
|
||||||
|
|
||||||
--color-blue-50: rgb(237, 245, 249);
|
|
||||||
--color-blue-100: rgb(218, 237, 248);
|
|
||||||
--color-blue-150: rgb(188, 225, 248);
|
|
||||||
--color-blue-200: rgb(156, 216, 253);
|
|
||||||
--color-blue-250: rgb(125, 204, 248);
|
|
||||||
--color-blue-300: rgb(97, 190, 241);
|
|
||||||
--color-blue-350: rgb(65, 178, 236);
|
|
||||||
--color-blue-400: rgb(36, 164, 223);
|
|
||||||
--color-blue-450: rgb(18, 148, 204);
|
|
||||||
--color-blue-500: rgb(21, 135, 186);
|
|
||||||
--color-blue-550: rgb(12, 121, 168);
|
|
||||||
--color-blue-600: rgb(11, 110, 153);
|
|
||||||
--color-blue-650: rgb(11, 97, 135);
|
|
||||||
--color-blue-700: rgb(17, 88, 121);
|
|
||||||
--color-blue-750: rgb(17, 76, 105);
|
|
||||||
--color-blue-800: rgb(18, 66, 90);
|
|
||||||
--color-blue-850: rgb(18, 56, 76);
|
|
||||||
--color-blue-900: rgb(19, 44, 58);
|
|
||||||
--color-blue-950: rgb(22, 33, 39);
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
@forward './queries.scss';
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
$breakpoint-xs-width: 400px;
|
|
||||||
$breakpoint-s-width: 768px;
|
|
||||||
$breakpoint-m-width: 1024px;
|
|
||||||
$breakpoint-l-width: 1440px;
|
|
||||||
|
|
||||||
@mixin extra-small-break {
|
|
||||||
@media (max-width: #{$breakpoint-xs-width}) {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin small-break {
|
|
||||||
@media (max-width: #{$breakpoint-s-width}) {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin mid-break {
|
|
||||||
@media (max-width: #{$breakpoint-m-width}) {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin large-break {
|
|
||||||
@media (max-width: #{$breakpoint-l-width}) {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
--theme-success-50: var(--color-success-50);
|
|
||||||
--theme-success-100: var(--color-success-100);
|
|
||||||
--theme-success-150: var(--color-success-150);
|
|
||||||
--theme-success-200: var(--color-success-200);
|
|
||||||
--theme-success-250: var(--color-success-250);
|
|
||||||
--theme-success-300: var(--color-success-300);
|
|
||||||
--theme-success-350: var(--color-success-350);
|
|
||||||
--theme-success-400: var(--color-success-400);
|
|
||||||
--theme-success-450: var(--color-success-450);
|
|
||||||
--theme-success-500: var(--color-success-500);
|
|
||||||
--theme-success-550: var(--color-success-550);
|
|
||||||
--theme-success-600: var(--color-success-600);
|
|
||||||
--theme-success-650: var(--color-success-650);
|
|
||||||
--theme-success-700: var(--color-success-700);
|
|
||||||
--theme-success-750: var(--color-success-750);
|
|
||||||
--theme-success-800: var(--color-success-800);
|
|
||||||
--theme-success-850: var(--color-success-850);
|
|
||||||
--theme-success-900: var(--color-success-900);
|
|
||||||
--theme-success-950: var(--color-success-950);
|
|
||||||
|
|
||||||
--theme-warning-50: var(--color-warning-50);
|
|
||||||
--theme-warning-100: var(--color-warning-100);
|
|
||||||
--theme-warning-150: var(--color-warning-150);
|
|
||||||
--theme-warning-200: var(--color-warning-200);
|
|
||||||
--theme-warning-250: var(--color-warning-250);
|
|
||||||
--theme-warning-300: var(--color-warning-300);
|
|
||||||
--theme-warning-350: var(--color-warning-350);
|
|
||||||
--theme-warning-400: var(--color-warning-400);
|
|
||||||
--theme-warning-450: var(--color-warning-450);
|
|
||||||
--theme-warning-500: var(--color-warning-500);
|
|
||||||
--theme-warning-550: var(--color-warning-550);
|
|
||||||
--theme-warning-600: var(--color-warning-600);
|
|
||||||
--theme-warning-650: var(--color-warning-650);
|
|
||||||
--theme-warning-700: var(--color-warning-700);
|
|
||||||
--theme-warning-750: var(--color-warning-750);
|
|
||||||
--theme-warning-800: var(--color-warning-800);
|
|
||||||
--theme-warning-850: var(--color-warning-850);
|
|
||||||
--theme-warning-900: var(--color-warning-900);
|
|
||||||
--theme-warning-950: var(--color-warning-950);
|
|
||||||
|
|
||||||
--theme-error-50: var(--color-error-50);
|
|
||||||
--theme-error-100: var(--color-error-100);
|
|
||||||
--theme-error-150: var(--color-error-150);
|
|
||||||
--theme-error-200: var(--color-error-200);
|
|
||||||
--theme-error-250: var(--color-error-250);
|
|
||||||
--theme-error-300: var(--color-error-300);
|
|
||||||
--theme-error-350: var(--color-error-350);
|
|
||||||
--theme-error-400: var(--color-error-400);
|
|
||||||
--theme-error-450: var(--color-error-450);
|
|
||||||
--theme-error-500: var(--color-error-500);
|
|
||||||
--theme-error-550: var(--color-error-550);
|
|
||||||
--theme-error-600: var(--color-error-600);
|
|
||||||
--theme-error-650: var(--color-error-650);
|
|
||||||
--theme-error-700: var(--color-error-700);
|
|
||||||
--theme-error-750: var(--color-error-750);
|
|
||||||
--theme-error-800: var(--color-error-800);
|
|
||||||
--theme-error-850: var(--color-error-850);
|
|
||||||
--theme-error-900: var(--color-error-900);
|
|
||||||
--theme-error-950: var(--color-error-950);
|
|
||||||
|
|
||||||
--theme-elevation-0: var(--color-base-0);
|
|
||||||
--theme-elevation-50: var(--color-base-50);
|
|
||||||
--theme-elevation-100: var(--color-base-100);
|
|
||||||
--theme-elevation-150: var(--color-base-150);
|
|
||||||
--theme-elevation-200: var(--color-base-200);
|
|
||||||
--theme-elevation-250: var(--color-base-250);
|
|
||||||
--theme-elevation-300: var(--color-base-300);
|
|
||||||
--theme-elevation-350: var(--color-base-350);
|
|
||||||
--theme-elevation-400: var(--color-base-400);
|
|
||||||
--theme-elevation-450: var(--color-base-450);
|
|
||||||
--theme-elevation-500: var(--color-base-500);
|
|
||||||
--theme-elevation-550: var(--color-base-550);
|
|
||||||
--theme-elevation-600: var(--color-base-600);
|
|
||||||
--theme-elevation-650: var(--color-base-650);
|
|
||||||
--theme-elevation-700: var(--color-base-700);
|
|
||||||
--theme-elevation-750: var(--color-base-750);
|
|
||||||
--theme-elevation-800: var(--color-base-800);
|
|
||||||
--theme-elevation-850: var(--color-base-850);
|
|
||||||
--theme-elevation-900: var(--color-base-900);
|
|
||||||
--theme-elevation-950: var(--color-base-950);
|
|
||||||
--theme-elevation-1000: var(--color-base-1000);
|
|
||||||
|
|
||||||
--theme-bg: var(--theme-elevation-0);
|
|
||||||
--theme-input-bg: var(--theme-elevation-50);
|
|
||||||
--theme-text: var(--theme-elevation-750);
|
|
||||||
--theme-border-color: var(--theme-elevation-150);
|
|
||||||
|
|
||||||
color-scheme: light;
|
|
||||||
color: var(--theme-text);
|
|
||||||
|
|
||||||
--highlight-default-bg-color: var(--theme-success-400);
|
|
||||||
--highlight-default-text-color: var(--theme-text);
|
|
||||||
|
|
||||||
--highlight-danger-bg-color: var(--theme-error-150);
|
|
||||||
--highlight-danger-text-color: var(--theme-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 a,
|
|
||||||
h2 a,
|
|
||||||
h3 a,
|
|
||||||
h4 a,
|
|
||||||
h5 a,
|
|
||||||
h6 a {
|
|
||||||
color: var(--theme-elevation-750);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--theme-elevation-800);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:visited {
|
|
||||||
color: var(--theme-elevation-750);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--theme-elevation-800);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--theme-elevation-0: var(--color-base-1000);
|
|
||||||
--theme-elevation-50: var(--color-base-950);
|
|
||||||
--theme-elevation-100: var(--color-base-900);
|
|
||||||
--theme-elevation-150: var(--color-base-850);
|
|
||||||
--theme-elevation-200: var(--color-base-800);
|
|
||||||
--theme-elevation-250: var(--color-base-750);
|
|
||||||
--theme-elevation-300: var(--color-base-700);
|
|
||||||
--theme-elevation-350: var(--color-base-650);
|
|
||||||
--theme-elevation-400: var(--color-base-600);
|
|
||||||
--theme-elevation-450: var(--color-base-550);
|
|
||||||
--theme-elevation-500: var(--color-base-500);
|
|
||||||
--theme-elevation-550: var(--color-base-450);
|
|
||||||
--theme-elevation-600: var(--color-base-400);
|
|
||||||
--theme-elevation-650: var(--color-base-350);
|
|
||||||
--theme-elevation-700: var(--color-base-300);
|
|
||||||
--theme-elevation-750: var(--color-base-250);
|
|
||||||
--theme-elevation-800: var(--color-base-200);
|
|
||||||
--theme-elevation-850: var(--color-base-150);
|
|
||||||
--theme-elevation-900: var(--color-base-100);
|
|
||||||
--theme-elevation-950: var(--color-base-50);
|
|
||||||
--theme-elevation-1000: var(--color-base-0);
|
|
||||||
|
|
||||||
--theme-success-50: var(--color-success-950);
|
|
||||||
--theme-success-100: var(--color-success-900);
|
|
||||||
--theme-success-150: var(--color-success-850);
|
|
||||||
--theme-success-200: var(--color-success-800);
|
|
||||||
--theme-success-250: var(--color-success-750);
|
|
||||||
--theme-success-300: var(--color-success-700);
|
|
||||||
--theme-success-350: var(--color-success-650);
|
|
||||||
--theme-success-400: var(--color-success-600);
|
|
||||||
--theme-success-450: var(--color-success-550);
|
|
||||||
--theme-success-500: var(--color-success-500);
|
|
||||||
--theme-success-550: var(--color-success-450);
|
|
||||||
--theme-success-600: var(--color-success-400);
|
|
||||||
--theme-success-650: var(--color-success-350);
|
|
||||||
--theme-success-700: var(--color-success-300);
|
|
||||||
--theme-success-750: var(--color-success-250);
|
|
||||||
--theme-success-800: var(--color-success-200);
|
|
||||||
--theme-success-850: var(--color-success-150);
|
|
||||||
--theme-success-900: var(--color-success-100);
|
|
||||||
--theme-success-950: var(--color-success-50);
|
|
||||||
|
|
||||||
--theme-warning-50: var(--color-warning-950);
|
|
||||||
--theme-warning-100: var(--color-warning-900);
|
|
||||||
--theme-warning-150: var(--color-warning-850);
|
|
||||||
--theme-warning-200: var(--color-warning-800);
|
|
||||||
--theme-warning-250: var(--color-warning-750);
|
|
||||||
--theme-warning-300: var(--color-warning-700);
|
|
||||||
--theme-warning-350: var(--color-warning-650);
|
|
||||||
--theme-warning-400: var(--color-warning-600);
|
|
||||||
--theme-warning-450: var(--color-warning-550);
|
|
||||||
--theme-warning-500: var(--color-warning-500);
|
|
||||||
--theme-warning-550: var(--color-warning-450);
|
|
||||||
--theme-warning-600: var(--color-warning-400);
|
|
||||||
--theme-warning-650: var(--color-warning-350);
|
|
||||||
--theme-warning-700: var(--color-warning-300);
|
|
||||||
--theme-warning-750: var(--color-warning-250);
|
|
||||||
--theme-warning-800: var(--color-warning-200);
|
|
||||||
--theme-warning-850: var(--color-warning-150);
|
|
||||||
--theme-warning-900: var(--color-warning-100);
|
|
||||||
--theme-warning-950: var(--color-warning-50);
|
|
||||||
|
|
||||||
--theme-error-50: var(--color-error-950);
|
|
||||||
--theme-error-100: var(--color-error-900);
|
|
||||||
--theme-error-150: var(--color-error-850);
|
|
||||||
--theme-error-200: var(--color-error-800);
|
|
||||||
--theme-error-250: var(--color-error-750);
|
|
||||||
--theme-error-300: var(--color-error-700);
|
|
||||||
--theme-error-350: var(--color-error-650);
|
|
||||||
--theme-error-400: var(--color-error-600);
|
|
||||||
--theme-error-450: var(--color-error-550);
|
|
||||||
--theme-error-500: var(--color-error-500);
|
|
||||||
--theme-error-550: var(--color-error-450);
|
|
||||||
--theme-error-600: var(--color-error-400);
|
|
||||||
--theme-error-650: var(--color-error-350);
|
|
||||||
--theme-error-700: var(--color-error-300);
|
|
||||||
--theme-error-750: var(--color-error-250);
|
|
||||||
--theme-error-800: var(--color-error-200);
|
|
||||||
--theme-error-850: var(--color-error-150);
|
|
||||||
--theme-error-900: var(--color-error-100);
|
|
||||||
--theme-error-950: var(--color-error-50);
|
|
||||||
|
|
||||||
--theme-bg: var(--theme-elevation-100);
|
|
||||||
--theme-text: var(--theme-elevation-900);
|
|
||||||
--theme-input-bg: var(--theme-elevation-150);
|
|
||||||
--theme-border-color: var(--theme-elevation-250);
|
|
||||||
|
|
||||||
color-scheme: dark;
|
|
||||||
color: var(--theme-text);
|
|
||||||
|
|
||||||
--highlight-default-bg-color: var(--theme-success-100);
|
|
||||||
--highlight-default-text-color: var(--theme-success-600);
|
|
||||||
|
|
||||||
--highlight-danger-bg-color: var(--theme-error-100);
|
|
||||||
--highlight-danger-text-color: var(--theme-error-550);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 a,
|
|
||||||
h2 a,
|
|
||||||
h3 a,
|
|
||||||
h4 a,
|
|
||||||
h5 a,
|
|
||||||
h6 a {
|
|
||||||
color: var(--theme-success-600);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--theme-success-400);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:visited {
|
|
||||||
color: var(--theme-success-700);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--theme-success-500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
@use 'queries' as *;
|
|
||||||
|
|
||||||
%h1,
|
|
||||||
%h2,
|
|
||||||
%h3,
|
|
||||||
%h4,
|
|
||||||
%h5,
|
|
||||||
%h6 {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
%h1 {
|
|
||||||
margin: 40px 0;
|
|
||||||
font-size: 64px;
|
|
||||||
line-height: 70px;
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
@include mid-break {
|
|
||||||
margin: 24px 0;
|
|
||||||
font-size: 42px;
|
|
||||||
line-height: 42px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
%h2 {
|
|
||||||
margin: 28px 0;
|
|
||||||
font-size: 48px;
|
|
||||||
line-height: 54px;
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
@include mid-break {
|
|
||||||
margin: 22px 0;
|
|
||||||
font-size: 32px;
|
|
||||||
line-height: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
%h3 {
|
|
||||||
margin: 24px 0;
|
|
||||||
font-size: 32px;
|
|
||||||
line-height: 40px;
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
@include mid-break {
|
|
||||||
margin: 20px 0;
|
|
||||||
font-size: 26px;
|
|
||||||
line-height: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
%h4 {
|
|
||||||
margin: 20px 0;
|
|
||||||
font-size: 26px;
|
|
||||||
line-height: 32px;
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
@include mid-break {
|
|
||||||
font-size: 22px;
|
|
||||||
line-height: 30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
%h5 {
|
|
||||||
margin: 20px 0;
|
|
||||||
font-size: 22px;
|
|
||||||
line-height: 30px;
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
@include mid-break {
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
%h6 {
|
|
||||||
margin: 20px 0;
|
|
||||||
font-size: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
%body {
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 32px;
|
|
||||||
|
|
||||||
@include mid-break {
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
%large-body {
|
|
||||||
font-size: 25px;
|
|
||||||
line-height: 32px;
|
|
||||||
|
|
||||||
@include mid-break {
|
|
||||||
font-size: 22px;
|
|
||||||
line-height: 30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
%label {
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 24px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
|
|
||||||
@include mid-break {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import type { AppProps } from 'next/app'
|
|
||||||
|
|
||||||
import { Header } from '../components/Header'
|
|
||||||
import { AuthProvider } from '../providers/Auth'
|
|
||||||
|
|
||||||
import '../css/app.scss'
|
|
||||||
|
|
||||||
export default function MyApp({ Component, pageProps }: AppProps) {
|
|
||||||
return (
|
|
||||||
<AuthProvider
|
|
||||||
// To toggle between the REST and GraphQL APIs,
|
|
||||||
// change the `api` prop to either `rest` or `gql`
|
|
||||||
api="rest" // change this to `gql` to use the GraphQL API
|
|
||||||
>
|
|
||||||
<Header />
|
|
||||||
{/* typescript flags this `@ts-expect-error` declaration as unneeded, but types are breaking the build process
|
|
||||||
Remove these comments when the issue is resolved
|
|
||||||
See more here: https://github.com/facebook/react/issues/24304
|
|
||||||
*/}
|
|
||||||
{/* @ts-expect-error */}
|
|
||||||
<Component {...pageProps} />
|
|
||||||
</AuthProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
@import '../../css/common';
|
|
||||||
|
|
||||||
.account {
|
|
||||||
margin-bottom: var(--block-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.params {
|
|
||||||
margin-top: var(--base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form {
|
|
||||||
margin-bottom: var(--base);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: calc(var(--base) / 2);
|
|
||||||
align-items: flex-start;
|
|
||||||
width: 66.66%;
|
|
||||||
|
|
||||||
@include mid-break {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.changePassword {
|
|
||||||
all: unset;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit {
|
|
||||||
margin-top: calc(var(--base) / 2);
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
|
|
||||||
import { Button } from '../../components/Button'
|
|
||||||
import { Gutter } from '../../components/Gutter'
|
|
||||||
import { Input } from '../../components/Input'
|
|
||||||
import { Message } from '../../components/Message'
|
|
||||||
import { RenderParams } from '../../components/RenderParams'
|
|
||||||
import { useAuth } from '../../providers/Auth'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
type FormData = {
|
|
||||||
email: string
|
|
||||||
name: string
|
|
||||||
password: string
|
|
||||||
passwordConfirm: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Account: React.FC = () => {
|
|
||||||
const [error, setError] = useState('')
|
|
||||||
const [success, setSuccess] = useState('')
|
|
||||||
const { user, setUser } = useAuth()
|
|
||||||
const [changePassword, setChangePassword] = useState(false)
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors, isLoading },
|
|
||||||
reset,
|
|
||||||
watch,
|
|
||||||
} = useForm<FormData>()
|
|
||||||
|
|
||||||
const password = useRef({})
|
|
||||||
password.current = watch('password', '')
|
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
|
||||||
async (data: FormData) => {
|
|
||||||
if (user) {
|
|
||||||
const response = await fetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users/${user.id}`,
|
|
||||||
{
|
|
||||||
// Make sure to include cookies with fetch
|
|
||||||
credentials: 'include',
|
|
||||||
method: 'PATCH',
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const json = await response.json()
|
|
||||||
setUser(json.doc)
|
|
||||||
setSuccess('Successfully updated account.')
|
|
||||||
setError('')
|
|
||||||
setChangePassword(false)
|
|
||||||
reset({
|
|
||||||
email: json.doc.email,
|
|
||||||
name: json.doc.name,
|
|
||||||
password: '',
|
|
||||||
passwordConfirm: '',
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setError('There was a problem updating your account.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[user, setUser, reset],
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user === null) {
|
|
||||||
router.push(`/login?unauthorized=account`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Once user is loaded, reset form to have default values
|
|
||||||
if (user) {
|
|
||||||
reset({
|
|
||||||
email: user.email,
|
|
||||||
password: '',
|
|
||||||
passwordConfirm: '',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [user, router, reset, changePassword])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Gutter className={classes.account}>
|
|
||||||
<RenderParams className={classes.params} />
|
|
||||||
<h1>Account</h1>
|
|
||||||
<p>
|
|
||||||
{`This is your account dashboard. Here you can update your account information and more. To manage all users, `}
|
|
||||||
<Link href={`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/admin/collections/users`}>
|
|
||||||
login to the admin dashboard
|
|
||||||
</Link>
|
|
||||||
{'.'}
|
|
||||||
</p>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
|
||||||
<Message error={error} success={success} className={classes.message} />
|
|
||||||
{!changePassword ? (
|
|
||||||
<Fragment>
|
|
||||||
<p>
|
|
||||||
{'Change your account details below, or '}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={classes.changePassword}
|
|
||||||
onClick={() => setChangePassword(!changePassword)}
|
|
||||||
>
|
|
||||||
click here
|
|
||||||
</button>
|
|
||||||
{' to change your password.'}
|
|
||||||
</p>
|
|
||||||
<Input
|
|
||||||
name="email"
|
|
||||||
label="Email Address"
|
|
||||||
required
|
|
||||||
register={register}
|
|
||||||
error={errors.email}
|
|
||||||
type="email"
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
) : (
|
|
||||||
<Fragment>
|
|
||||||
<p>
|
|
||||||
{'Change your password below, or '}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={classes.changePassword}
|
|
||||||
onClick={() => setChangePassword(!changePassword)}
|
|
||||||
>
|
|
||||||
cancel
|
|
||||||
</button>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
<Input
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
label="Password"
|
|
||||||
required
|
|
||||||
register={register}
|
|
||||||
error={errors.password}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
name="passwordConfirm"
|
|
||||||
type="password"
|
|
||||||
label="Confirm Password"
|
|
||||||
required
|
|
||||||
register={register}
|
|
||||||
validate={(value) => value === password.current || 'The passwords do not match'}
|
|
||||||
error={errors.passwordConfirm}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className={classes.submit}
|
|
||||||
label={isLoading ? 'Processing' : changePassword ? 'Change password' : 'Update account'}
|
|
||||||
appearance="primary"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
<Button href="/logout" appearance="secondary" label="Log out" />
|
|
||||||
</Gutter>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Account
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
@import '../../css/common';
|
|
||||||
|
|
||||||
.createAccount {
|
|
||||||
margin-bottom: var(--block-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form {
|
|
||||||
margin-bottom: var(--base);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: calc(var(--base) / 2);
|
|
||||||
align-items: flex-start;
|
|
||||||
width: 66.66%;
|
|
||||||
|
|
||||||
@include mid-break {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit {
|
|
||||||
margin-top: calc(var(--base) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
margin-bottom: var(--base);
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
|
|
||||||
import { Button } from '../../components/Button'
|
|
||||||
import { Gutter } from '../../components/Gutter'
|
|
||||||
import { Input } from '../../components/Input'
|
|
||||||
import { Message } from '../../components/Message'
|
|
||||||
import { RenderParams } from '../../components/RenderParams'
|
|
||||||
import { useAuth } from '../../providers/Auth'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
type FormData = {
|
|
||||||
email: string
|
|
||||||
password: string
|
|
||||||
passwordConfirm: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const CreateAccount: React.FC = () => {
|
|
||||||
const router = useRouter()
|
|
||||||
const searchParams = useMemo(() => new URLSearchParams(router.query as any), [router.query])
|
|
||||||
const allParams = searchParams.toString() ? `?${searchParams.toString()}` : ''
|
|
||||||
const { login } = useAuth()
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
|
||||||
watch,
|
|
||||||
} = useForm<FormData>()
|
|
||||||
|
|
||||||
const password = useRef({})
|
|
||||||
password.current = watch('password', '')
|
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
|
||||||
async (data: FormData) => {
|
|
||||||
const response = await fetch(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const message = response.statusText || 'There was an error creating the account.'
|
|
||||||
setError(message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const redirect = searchParams.get('redirect')
|
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setLoading(true)
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
try {
|
|
||||||
await login(data)
|
|
||||||
clearTimeout(timer)
|
|
||||||
if (redirect) router.push(redirect as string)
|
|
||||||
else router.push(`/account?success=${encodeURIComponent('Account created successfully')}`)
|
|
||||||
} catch (_) {
|
|
||||||
clearTimeout(timer)
|
|
||||||
setError('There was an error with the credentials provided. Please try again.')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[login, router, searchParams],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Gutter className={classes.createAccount}>
|
|
||||||
<h1>Create Account</h1>
|
|
||||||
<RenderParams />
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
|
||||||
<p>
|
|
||||||
{`This is where new customers can signup and create a new account. To manage all users, `}
|
|
||||||
<Link href={`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/admin/collections/users`}>
|
|
||||||
login to the admin dashboard
|
|
||||||
</Link>
|
|
||||||
{'.'}
|
|
||||||
</p>
|
|
||||||
<Message error={error} className={classes.message} />
|
|
||||||
<Input
|
|
||||||
name="email"
|
|
||||||
label="Email Address"
|
|
||||||
required
|
|
||||||
register={register}
|
|
||||||
error={errors.email}
|
|
||||||
type="email"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
label="Password"
|
|
||||||
required
|
|
||||||
register={register}
|
|
||||||
error={errors.password}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
name="passwordConfirm"
|
|
||||||
type="password"
|
|
||||||
label="Confirm Password"
|
|
||||||
required
|
|
||||||
register={register}
|
|
||||||
validate={(value) => value === password.current || 'The passwords do not match'}
|
|
||||||
error={errors.passwordConfirm}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className={classes.submit}
|
|
||||||
label={loading ? 'Processing' : 'Create Account'}
|
|
||||||
appearance="primary"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
{'Already have an account? '}
|
|
||||||
<Link href={`/login${allParams}`}>Login</Link>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Gutter>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CreateAccount
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
import { Gutter } from '../components/Gutter'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<Gutter>
|
|
||||||
<h1>Payload Auth Example</h1>
|
|
||||||
<p>
|
|
||||||
{'This is a '}
|
|
||||||
<Link href="https://payloadcms.com" target="_blank" rel="noopener noreferrer">
|
|
||||||
Payload
|
|
||||||
</Link>
|
|
||||||
{' + '}
|
|
||||||
<Link href="https://nextjs.org" target="_blank" rel="noopener noreferrer">
|
|
||||||
Next.js
|
|
||||||
</Link>
|
|
||||||
{' app using the '}
|
|
||||||
<Link href="https://nextjs.org/docs/pages" target="_blank" rel="noopener noreferrer">
|
|
||||||
Pages Router
|
|
||||||
</Link>
|
|
||||||
{' made explicitly for the '}
|
|
||||||
<Link href="https://github.com/payloadcms/payload/tree/main/examples/auth">
|
|
||||||
Payload Auth Example
|
|
||||||
</Link>
|
|
||||||
{". This example demonstrates how to implement Payload's "}
|
|
||||||
<Link href="https://payloadcms.com/docs/authentication/overview">Authentication</Link>
|
|
||||||
{
|
|
||||||
' strategies through HTTP using the REST and GraphQL APIs. To toggle between these two APIs, see `_app.tsx`.'
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{'Visit the '}
|
|
||||||
<Link href="/login">login page</Link>
|
|
||||||
{' to start the authentication flow. Once logged in, you will be redirected to the '}
|
|
||||||
<Link href="/account">account page</Link>
|
|
||||||
{` which is restricted to users only. To manage all users, `}
|
|
||||||
<Link href={`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/admin/collections/users`}>
|
|
||||||
login to the admin dashboard
|
|
||||||
</Link>
|
|
||||||
{'.'}
|
|
||||||
</p>
|
|
||||||
</Gutter>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
@import '../../css/common';
|
|
||||||
|
|
||||||
.login {
|
|
||||||
margin-bottom: var(--block-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.params {
|
|
||||||
margin-top: var(--base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form {
|
|
||||||
margin-bottom: var(--base);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: calc(var(--base) / 2);
|
|
||||||
align-items: flex-start;
|
|
||||||
width: 66.66%;
|
|
||||||
|
|
||||||
@include mid-break {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit {
|
|
||||||
margin-top: calc(var(--base) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
margin-bottom: var(--base);
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import React, { useCallback, useMemo, useRef } from 'react'
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
|
|
||||||
import { Button } from '../../components/Button'
|
|
||||||
import { Gutter } from '../../components/Gutter'
|
|
||||||
import { Input } from '../../components/Input'
|
|
||||||
import { Message } from '../../components/Message'
|
|
||||||
import { RenderParams } from '../../components/RenderParams'
|
|
||||||
import { useAuth } from '../../providers/Auth'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
type FormData = {
|
|
||||||
email: string
|
|
||||||
password: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Login: React.FC = () => {
|
|
||||||
const router = useRouter()
|
|
||||||
const searchParams = useMemo(() => new URLSearchParams(router.query as any), [router.query])
|
|
||||||
const allParams = searchParams.toString() ? `?${searchParams.toString()}` : ''
|
|
||||||
const redirect = useRef(searchParams.get('redirect'))
|
|
||||||
const { login } = useAuth()
|
|
||||||
const [error, setError] = React.useState<string | null>(null)
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors, isLoading },
|
|
||||||
} = useForm<FormData>({
|
|
||||||
defaultValues: {
|
|
||||||
email: 'demo@payloadcms.com',
|
|
||||||
password: 'demo',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
|
||||||
async (data: FormData) => {
|
|
||||||
try {
|
|
||||||
await login(data)
|
|
||||||
if (redirect?.current) router.push(redirect.current as string)
|
|
||||||
else router.push('/account')
|
|
||||||
} catch (_) {
|
|
||||||
setError('There was an error with the credentials provided. Please try again.')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[login, router],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Gutter className={classes.login}>
|
|
||||||
<RenderParams className={classes.params} />
|
|
||||||
<h1>Log in</h1>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
|
||||||
<p>
|
|
||||||
{'To log in, use the email '}
|
|
||||||
<b>demo@payloadcms.com</b>
|
|
||||||
{' with the password '}
|
|
||||||
<b>demo</b>
|
|
||||||
{'. To manage your users, '}
|
|
||||||
<Link href={`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/admin/collections/users`}>
|
|
||||||
login to the admin dashboard
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
<Message error={error} className={classes.message} />
|
|
||||||
<Input
|
|
||||||
name="email"
|
|
||||||
label="Email Address"
|
|
||||||
required
|
|
||||||
register={register}
|
|
||||||
error={errors.email}
|
|
||||||
type="email"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
label="Password"
|
|
||||||
required
|
|
||||||
register={register}
|
|
||||||
error={errors.password}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading}
|
|
||||||
className={classes.submit}
|
|
||||||
label={isLoading ? 'Processing' : 'Login'}
|
|
||||||
appearance="primary"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<Link href={`/create-account${allParams}`}>Create an account</Link>
|
|
||||||
<br />
|
|
||||||
<Link href={`/recover-password${allParams}`}>Recover your password</Link>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Gutter>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Login
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
.logout {
|
|
||||||
margin-bottom: var(--block-padding);
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
import { Gutter } from '../../components/Gutter'
|
|
||||||
import { useAuth } from '../../providers/Auth'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
const Logout: React.FC = () => {
|
|
||||||
const { logout } = useAuth()
|
|
||||||
const [success, setSuccess] = useState('')
|
|
||||||
const [error, setError] = useState('')
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const performLogout = async () => {
|
|
||||||
try {
|
|
||||||
await logout()
|
|
||||||
setSuccess('Logged out successfully.')
|
|
||||||
} catch (_) {
|
|
||||||
setError('You are already logged out.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
performLogout()
|
|
||||||
}, [logout])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Gutter className={classes.logout}>
|
|
||||||
{(error || success) && (
|
|
||||||
<div>
|
|
||||||
<h1>{error || success}</h1>
|
|
||||||
<p>
|
|
||||||
{'What would you like to do next? '}
|
|
||||||
<Link href="/">Click here</Link>
|
|
||||||
{` to go to the home page. To log back in, `}
|
|
||||||
<Link href="/login">click here</Link>
|
|
||||||
{'.'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Gutter>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Logout
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
@import '../../css/common';
|
|
||||||
|
|
||||||
.recoverPassword {
|
|
||||||
margin-bottom: var(--block-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: red;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formWrapper {
|
|
||||||
width: 66.66%;
|
|
||||||
|
|
||||||
@include mid-break {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit {
|
|
||||||
margin-top: var(--base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
margin-bottom: var(--base);
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import React, { useCallback, useState } from 'react'
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
import Link from 'next/link'
|
|
||||||
|
|
||||||
import { Button } from '../../components/Button'
|
|
||||||
import { Gutter } from '../../components/Gutter'
|
|
||||||
import { Input } from '../../components/Input'
|
|
||||||
import { Message } from '../../components/Message'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
type FormData = {
|
|
||||||
email: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const RecoverPassword: React.FC = () => {
|
|
||||||
const [error, setError] = useState('')
|
|
||||||
const [success, setSuccess] = useState(false)
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<FormData>()
|
|
||||||
|
|
||||||
const onSubmit = useCallback(async (data: FormData) => {
|
|
||||||
const response = await fetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users/forgot-password`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
setSuccess(true)
|
|
||||||
setError('')
|
|
||||||
} else {
|
|
||||||
setError(
|
|
||||||
'There was a problem while attempting to send you a password reset email. Please try again.',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Gutter className={classes.recoverPassword}>
|
|
||||||
{!success && (
|
|
||||||
<React.Fragment>
|
|
||||||
<h1>Recover Password</h1>
|
|
||||||
<div className={classes.formWrapper}>
|
|
||||||
<p>
|
|
||||||
{`Please enter your email below. You will receive an email message with instructions on
|
|
||||||
how to reset your password. To manage all of your users, `}
|
|
||||||
<Link href={`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/admin/collections/users`}>
|
|
||||||
login to the admin dashboard
|
|
||||||
</Link>
|
|
||||||
{'.'}
|
|
||||||
</p>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
|
||||||
<Message error={error} className={classes.message} />
|
|
||||||
<Input
|
|
||||||
name="email"
|
|
||||||
label="Email Address"
|
|
||||||
required
|
|
||||||
register={register}
|
|
||||||
error={errors.email}
|
|
||||||
type="email"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className={classes.submit}
|
|
||||||
label="Recover Password"
|
|
||||||
appearance="primary"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</React.Fragment>
|
|
||||||
)}
|
|
||||||
{success && (
|
|
||||||
<React.Fragment>
|
|
||||||
<h1>Request submitted</h1>
|
|
||||||
<p>Check your email for a link that will allow you to securely reset your password.</p>
|
|
||||||
</React.Fragment>
|
|
||||||
)}
|
|
||||||
</Gutter>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RecoverPassword
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
@import '../../css/common';
|
|
||||||
|
|
||||||
.resetPassword {
|
|
||||||
margin-bottom: var(--block-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form {
|
|
||||||
width: 66.66%;
|
|
||||||
|
|
||||||
@include mid-break {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit {
|
|
||||||
margin-top: var(--base);
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
|
|
||||||
import { Button } from '../../components/Button'
|
|
||||||
import { Gutter } from '../../components/Gutter'
|
|
||||||
import { Input } from '../../components/Input'
|
|
||||||
import { Message } from '../../components/Message'
|
|
||||||
import { useAuth } from '../../providers/Auth'
|
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
type FormData = {
|
|
||||||
password: string
|
|
||||||
token: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const ResetPassword: React.FC = () => {
|
|
||||||
const [error, setError] = useState('')
|
|
||||||
const { login } = useAuth()
|
|
||||||
const router = useRouter()
|
|
||||||
const searchParams = useMemo(() => new URLSearchParams(router.query as any), [router.query])
|
|
||||||
const token = searchParams.get('token')
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
|
||||||
reset,
|
|
||||||
} = useForm<FormData>()
|
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
|
||||||
async (data: FormData) => {
|
|
||||||
const response = await fetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users/reset-password`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const json = await response.json()
|
|
||||||
|
|
||||||
// Automatically log the user in after they successfully reset password
|
|
||||||
await login({ email: json.user.email, password: data.password })
|
|
||||||
|
|
||||||
// Redirect them to `/account` with success message in URL
|
|
||||||
router.push('/account?success=Password reset successfully.')
|
|
||||||
} else {
|
|
||||||
setError('There was a problem while resetting your password. Please try again later.')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[router, login],
|
|
||||||
)
|
|
||||||
|
|
||||||
// when Next.js populates token within router,
|
|
||||||
// reset form with new token value
|
|
||||||
useEffect(() => {
|
|
||||||
reset({ token: token || undefined })
|
|
||||||
}, [reset, token])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Gutter className={classes.resetPassword}>
|
|
||||||
<h1>Reset Password</h1>
|
|
||||||
<p>Please enter a new password below.</p>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
|
||||||
<Message error={error} className={classes.message} />
|
|
||||||
<Input
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
label="New Password"
|
|
||||||
required
|
|
||||||
register={register}
|
|
||||||
error={errors.password}
|
|
||||||
/>
|
|
||||||
<input type="hidden" {...register('token')} />
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className={classes.submit}
|
|
||||||
label="Reset Password"
|
|
||||||
appearance="primary"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Gutter>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ResetPassword
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
/**
|
|
||||||
* This file was automatically generated by Payload.
|
|
||||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
|
||||||
* and re-run `payload generate:types` to regenerate this file.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface Config {
|
|
||||||
collections: {
|
|
||||||
users: User
|
|
||||||
}
|
|
||||||
globals: {}
|
|
||||||
}
|
|
||||||
export interface User {
|
|
||||||
id: string
|
|
||||||
firstName?: string
|
|
||||||
lastName?: string
|
|
||||||
roles?: ('admin' | 'user')[]
|
|
||||||
updatedAt: string
|
|
||||||
createdAt: string
|
|
||||||
email: string
|
|
||||||
resetPasswordToken?: string
|
|
||||||
resetPasswordExpiration?: string
|
|
||||||
salt?: string
|
|
||||||
hash?: string
|
|
||||||
loginAttempts?: number
|
|
||||||
lockUntil?: string
|
|
||||||
password?: string
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
export const USER = `
|
|
||||||
id
|
|
||||||
email
|
|
||||||
firstName
|
|
||||||
lastName
|
|
||||||
`
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export const gql = async (query): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/graphql`, {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
query,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const { data, errors } = await res.json()
|
|
||||||
|
|
||||||
if (errors) {
|
|
||||||
throw new Error(errors[0].message)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.ok && data) {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
} catch (e: unknown) {
|
|
||||||
throw new Error(e as string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
import { User } from '../../payload-types'
|
|
||||||
import { gql, USER } from './gql'
|
|
||||||
import { rest } from './rest'
|
|
||||||
import { AuthContext, Create, ForgotPassword, Login, Logout, ResetPassword } from './types'
|
|
||||||
|
|
||||||
const Context = createContext({} as AuthContext)
|
|
||||||
|
|
||||||
export const AuthProvider: React.FC<{ children: React.ReactNode; api?: 'rest' | 'gql' }> = ({
|
|
||||||
children,
|
|
||||||
api = 'rest',
|
|
||||||
}) => {
|
|
||||||
const [user, setUser] = useState<User | null>()
|
|
||||||
|
|
||||||
const create = useCallback<Create>(
|
|
||||||
async (args) => {
|
|
||||||
if (api === 'rest') {
|
|
||||||
const user = await rest(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users`, args)
|
|
||||||
setUser(user)
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
if (api === 'gql') {
|
|
||||||
const { createUser: user } = await gql(`mutation {
|
|
||||||
createUser(data: { email: "${args.email}", password: "${args.password}", firstName: "${args.firstName}", lastName: "${args.lastName}" }) {
|
|
||||||
${USER}
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
setUser(user)
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[api],
|
|
||||||
)
|
|
||||||
|
|
||||||
const login = useCallback<Login>(
|
|
||||||
async (args) => {
|
|
||||||
if (api === 'rest') {
|
|
||||||
const user = await rest(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users/login`, args)
|
|
||||||
setUser(user)
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
if (api === 'gql') {
|
|
||||||
const { loginUser } = await gql(`mutation {
|
|
||||||
loginUser(email: "${args.email}", password: "${args.password}") {
|
|
||||||
user {
|
|
||||||
${USER}
|
|
||||||
}
|
|
||||||
exp
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
setUser(loginUser?.user)
|
|
||||||
return loginUser?.user
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[api],
|
|
||||||
)
|
|
||||||
|
|
||||||
const logout = useCallback<Logout>(async () => {
|
|
||||||
if (api === 'rest') {
|
|
||||||
await rest(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users/logout`)
|
|
||||||
setUser(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (api === 'gql') {
|
|
||||||
await gql(`mutation {
|
|
||||||
logoutUser
|
|
||||||
}`)
|
|
||||||
|
|
||||||
setUser(null)
|
|
||||||
}
|
|
||||||
}, [api])
|
|
||||||
|
|
||||||
// On mount, get user and set
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchMe = async () => {
|
|
||||||
if (api === 'rest') {
|
|
||||||
const user = await rest(
|
|
||||||
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users/me`,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
setUser(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (api === 'gql') {
|
|
||||||
const { meUser } = await gql(`query {
|
|
||||||
meUser {
|
|
||||||
user {
|
|
||||||
${USER}
|
|
||||||
}
|
|
||||||
exp
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
setUser(meUser.user)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchMe()
|
|
||||||
}, [api])
|
|
||||||
|
|
||||||
const forgotPassword = useCallback<ForgotPassword>(
|
|
||||||
async (args) => {
|
|
||||||
if (api === 'rest') {
|
|
||||||
const user = await rest(
|
|
||||||
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users/forgot-password`,
|
|
||||||
args,
|
|
||||||
)
|
|
||||||
setUser(user)
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
if (api === 'gql') {
|
|
||||||
const { forgotPasswordUser } = await gql(`mutation {
|
|
||||||
forgotPasswordUser(email: "${args.email}")
|
|
||||||
}`)
|
|
||||||
|
|
||||||
return forgotPasswordUser
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[api],
|
|
||||||
)
|
|
||||||
|
|
||||||
const resetPassword = useCallback<ResetPassword>(
|
|
||||||
async (args) => {
|
|
||||||
if (api === 'rest') {
|
|
||||||
const user = await rest(
|
|
||||||
`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/users/reset-password`,
|
|
||||||
args,
|
|
||||||
)
|
|
||||||
setUser(user)
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
if (api === 'gql') {
|
|
||||||
const { resetPasswordUser } = await gql(`mutation {
|
|
||||||
resetPasswordUser(password: "${args.password}", token: "${args.token}") {
|
|
||||||
user {
|
|
||||||
${USER}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
setUser(resetPasswordUser.user)
|
|
||||||
return resetPasswordUser.user
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[api],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Context.Provider
|
|
||||||
value={{
|
|
||||||
user,
|
|
||||||
setUser,
|
|
||||||
login,
|
|
||||||
logout,
|
|
||||||
create,
|
|
||||||
resetPassword,
|
|
||||||
forgotPassword,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Context.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type UseAuth<T = User> = () => AuthContext // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
export const useAuth: UseAuth = () => useContext(Context)
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import type { User } from '../../payload-types'
|
|
||||||
|
|
||||||
export const rest = async (
|
|
||||||
url: string,
|
|
||||||
args?: any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
||||||
options?: RequestInit,
|
|
||||||
): Promise<User | null> => {
|
|
||||||
const method = options?.method || 'POST'
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method,
|
|
||||||
...(method === 'POST' ? { body: JSON.stringify(args) } : {}),
|
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options?.headers,
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
|
|
||||||
const { errors, user } = await res.json()
|
|
||||||
|
|
||||||
if (errors) {
|
|
||||||
throw new Error(errors[0].message)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
} catch (e: unknown) {
|
|
||||||
throw new Error(e as string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import type { User } from '../../payload-types'
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
export type ResetPassword = (args: {
|
|
||||||
password: string
|
|
||||||
passwordConfirm: string
|
|
||||||
token: string
|
|
||||||
}) => Promise<User>
|
|
||||||
|
|
||||||
export type ForgotPassword = (args: { email: string }) => Promise<User> // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
export type Create = (args: {
|
|
||||||
email: string
|
|
||||||
password: string
|
|
||||||
firstName: string
|
|
||||||
lastName: string
|
|
||||||
}) => Promise<User> // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
export type Login = (args: { email: string; password: string }) => Promise<User> // eslint-disable-line no-unused-vars
|
|
||||||
|
|
||||||
export type Logout = () => Promise<void>
|
|
||||||
|
|
||||||
export interface AuthContext {
|
|
||||||
user?: User | null
|
|
||||||
setUser: (user: User | null) => void // eslint-disable-line no-unused-vars
|
|
||||||
logout: Logout
|
|
||||||
login: Login
|
|
||||||
create: Create
|
|
||||||
resetPassword: ResetPassword
|
|
||||||
forgotPassword: ForgotPassword
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2022",
|
|
||||||
"lib": [
|
|
||||||
"DOM",
|
|
||||||
"DOM.Iterable",
|
|
||||||
"ES2022"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"strict": false,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"incremental": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"jsx": "preserve"
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"next-env.d.ts",
|
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user