chore: updates auth example (#3026)
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
# Payload Auth Example
|
||||
|
||||
This example demonstrates how to implement [Payload Authentication](https://payloadcms.com/docs/authentication/overview).
|
||||
This example demonstrates how to implement [Payload Authentication](https://payloadcms.com/docs/authentication/overview). Follow the [Quick Start](#quick-start) to get up and running quickly. There are various fully working front-ends made explicitly for this example, including:
|
||||
|
||||
There is a fully working Next.js app made explicitly for this example which can be found [here](../next-app). Follow the instructions there to get started. If you are setting up authentication for another front-end, please consider contributing to this repo with your own example!
|
||||
- [Next.js App Router](../next-app)
|
||||
- [Next.js Pages Router](../next-pages)
|
||||
|
||||
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!
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Payload Auth Example Front-End
|
||||
|
||||
This is a [Next.js](https://nextjs.org) app using the [App Router](https://nextjs.org/docs/app). It was made explicitly for Payload's [Auth Example](https://github.com/payloadcms/payload/tree/master/examples/auth/cms).
|
||||
This is a [Payload](https://payloadcms.com) + [Next.js](https://nextjs.org) app using the [App Router](https://nextjs.org/docs/app) made explicitly for the [Payload Auth Example](https://github.com/payloadcms/payload/tree/master/examples/auth). It 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/master/examples/auth/next-pages).
|
||||
|
||||
@@ -8,7 +8,7 @@ This is a [Next.js](https://nextjs.org) app using the [App Router](https://nextj
|
||||
|
||||
### Payload
|
||||
|
||||
First you'll need a running [Payload](https://github.com/payloadcms/payload) app. If you have not done so already, open up the `cms` folder and follow the setup instructions. Take note of your `serverURL`, you'll need this in the next step.
|
||||
First you'll need a running Payload app. If you have not done so already, clone down the [`cms`](../cms) folder and follow the setup instructions there to get it up and running. This will provide all the necessary APIs that your Next.js app will be using for authentication.
|
||||
|
||||
### Next.js
|
||||
|
||||
@@ -18,7 +18,7 @@ First you'll need a running [Payload](https://github.com/payloadcms/payload) app
|
||||
4. `yarn dev` or `npm run dev` to start the server
|
||||
5. `open http://localhost:3001` to see the result
|
||||
|
||||
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Auth Example](https://github.com/payloadcms/payload/tree/master/examples/auth/cms) for full details.
|
||||
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/master/examples/auth) for full details.
|
||||
|
||||
## Learn More
|
||||
|
||||
@@ -35,3 +35,7 @@ You can check out [the Payload GitHub repository](https://github.com/payloadcms/
|
||||
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/master/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).
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
@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);
|
||||
}
|
||||
75
examples/auth/next-app/app/_components/Button/index.tsx
Normal file
75
examples/auth/next-app/app/_components/Button/index.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
'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,7 +1,7 @@
|
||||
.gutter {
|
||||
max-width: var(--max-width);
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
max-width: 1920px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.gutterLeft {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
@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;
|
||||
}
|
||||
38
examples/auth/next-app/app/_components/Header/Nav/index.tsx
Normal file
38
examples/auth/next-app/app/_components/Header/Nav/index.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
'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,31 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { Fragment } from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useAuth } from '../Auth'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export function HeaderClient() {
|
||||
const { user } = useAuth()
|
||||
|
||||
return (
|
||||
<nav className={classes.nav}>
|
||||
{!user && (
|
||||
<Fragment>
|
||||
<Link href="/login">Login</Link>
|
||||
<Link href="/create-account">Create Account</Link>
|
||||
</Fragment>
|
||||
)}
|
||||
{user && (
|
||||
<Fragment>
|
||||
<Link href="/account">Account</Link>
|
||||
<Link href="/logout">Logout</Link>
|
||||
</Fragment>
|
||||
)}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default HeaderClient
|
||||
@@ -1,3 +1,5 @@
|
||||
@use '../../_css/queries.scss' as *;
|
||||
|
||||
.header {
|
||||
padding: var(--base) 0;
|
||||
}
|
||||
@@ -5,28 +7,16 @@
|
||||
.wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: calc(var(--base) / 2);
|
||||
flex-wrap: wrap;
|
||||
gap: calc(var(--base) / 2) var(--base);
|
||||
}
|
||||
|
||||
.logo {
|
||||
flex-shrink: 0;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--base);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
flex-wrap: wrap;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
gap: 0 calc(var(--base) / 2);
|
||||
:global([data-theme="light"]) {
|
||||
.logo {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { Gutter } from '../Gutter'
|
||||
import { HeaderClient } from './index.client'
|
||||
import { HeaderNav } from './Nav'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
@@ -25,7 +25,7 @@ export function Header() {
|
||||
/>
|
||||
</picture>
|
||||
</Link>
|
||||
<HeaderClient />
|
||||
<HeaderNav />
|
||||
</Gutter>
|
||||
</header>
|
||||
)
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
.input {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.input input {
|
||||
width: 100%;
|
||||
font-family: system-ui;
|
||||
border-radius: 0;
|
||||
box-shadow: 0;
|
||||
border: 1px solid #d8d8d8;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-top: 5px;
|
||||
color: red;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
@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,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { FieldValues, UseFormRegister } from 'react-hook-form'
|
||||
import { FieldValues, UseFormRegister, Validate } from 'react-hook-form'
|
||||
|
||||
import classes from './index.module.css'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type Props = {
|
||||
name: string
|
||||
@@ -9,7 +9,8 @@ type Props = {
|
||||
register: UseFormRegister<FieldValues & any>
|
||||
required?: boolean
|
||||
error: any
|
||||
type?: 'text' | 'number' | 'password'
|
||||
type?: 'text' | 'number' | 'password' | 'email'
|
||||
validate?: (value: string) => boolean | string
|
||||
}
|
||||
|
||||
export const Input: React.FC<Props> = ({
|
||||
@@ -19,14 +20,36 @@ export const Input: React.FC<Props> = ({
|
||||
register,
|
||||
error,
|
||||
type = 'text',
|
||||
validate,
|
||||
}) => {
|
||||
return (
|
||||
<div className={classes.input}>
|
||||
<div className={classes.inputWrap}>
|
||||
<label htmlFor="name" className={classes.label}>
|
||||
{label}
|
||||
{`${label} ${required ? '*' : ''}`}
|
||||
</label>
|
||||
<input {...{ type }} {...register(name, { required })} />
|
||||
{error && <div className={classes.error}>This field is required</div>}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
@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);
|
||||
}
|
||||
}
|
||||
33
examples/auth/next-app/app/_components/Message/index.tsx
Normal file
33
examples/auth/next-app/app/_components/Message/index.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
'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
|
||||
}
|
||||
117
examples/auth/next-app/app/_css/app.scss
Normal file
117
examples/auth/next-app/app/_css/app.scss
Normal file
@@ -0,0 +1,117 @@
|
||||
@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) * .75) 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: var(--base);
|
||||
margin: 0 0 var(--base);
|
||||
}
|
||||
|
||||
a {
|
||||
color: currentColor;
|
||||
|
||||
&:focus {
|
||||
opacity: .8;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: .7;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
83
examples/auth/next-app/app/_css/colors.scss
Normal file
83
examples/auth/next-app/app/_css/colors.scss
Normal file
@@ -0,0 +1,83 @@
|
||||
: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(247, 255, 251);
|
||||
--color-success-100: rgb(240, 255, 247);
|
||||
--color-success-150: rgb(232, 255, 243);
|
||||
--color-success-200: rgb(224, 255, 239);
|
||||
--color-success-250: rgb(217, 255, 235);
|
||||
--color-success-300: rgb(209, 255, 230);
|
||||
--color-success-350: rgb(201, 255, 226);
|
||||
--color-success-400: rgb(193, 255, 222);
|
||||
--color-success-450: rgb(186, 255, 218);
|
||||
--color-success-500: rgb(178, 255, 214);
|
||||
--color-success-550: rgb(160, 230, 193);
|
||||
--color-success-600: rgb(142, 204, 171);
|
||||
--color-success-650: rgb(125, 179, 150);
|
||||
--color-success-700: rgb(107, 153, 128);
|
||||
--color-success-750: rgb(89, 128, 107);
|
||||
--color-success-800: rgb(71, 102, 86);
|
||||
--color-success-850: rgb(53, 77, 64);
|
||||
--color-success-900: rgb(36, 51, 43);
|
||||
--color-success-950: rgb(18, 25, 21);
|
||||
|
||||
--color-warning-50: rgb(255, 255, 246);
|
||||
--color-warning-100: rgb(255, 255, 237);
|
||||
--color-warning-150: rgb(254, 255, 228);
|
||||
--color-warning-200: rgb(254, 255, 219);
|
||||
--color-warning-250: rgb(254, 255, 210);
|
||||
--color-warning-300: rgb(254, 255, 200);
|
||||
--color-warning-350: rgb(254, 255, 191);
|
||||
--color-warning-400: rgb(253, 255, 182);
|
||||
--color-warning-450: rgb(253, 255, 173);
|
||||
--color-warning-500: rgb(253, 255, 164);
|
||||
--color-warning-550: rgb(228, 230, 148);
|
||||
--color-warning-600: rgb(202, 204, 131);
|
||||
--color-warning-650: rgb(177, 179, 115);
|
||||
--color-warning-700: rgb(152, 153, 98);
|
||||
--color-warning-750: rgb(127, 128, 82);
|
||||
--color-warning-800: rgb(101, 102, 66);
|
||||
--color-warning-850: rgb(76, 77, 49);
|
||||
--color-warning-900: rgb(51, 51, 33);
|
||||
--color-warning-950: rgb(25, 25, 16);
|
||||
|
||||
--color-error-50: rgb(255, 241, 241);
|
||||
--color-error-100: rgb(255, 226, 228);
|
||||
--color-error-150: rgb(255, 212, 214);
|
||||
--color-error-200: rgb(255, 197, 200);
|
||||
--color-error-250: rgb(255, 183, 187);
|
||||
--color-error-300: rgb(255, 169, 173);
|
||||
--color-error-350: rgb(255, 154, 159);
|
||||
--color-error-400: rgb(255, 140, 145);
|
||||
--color-error-450: rgb(255, 125, 132);
|
||||
--color-error-500: rgb(255, 111, 118);
|
||||
--color-error-550: rgb(230, 100, 106);
|
||||
--color-error-600: rgb(204, 89, 94);
|
||||
--color-error-650: rgb(179, 78, 83);
|
||||
--color-error-700: rgb(153, 67, 71);
|
||||
--color-error-750: rgb(128, 56, 59);
|
||||
--color-error-800: rgb(102, 44, 47);
|
||||
--color-error-850: rgb(77, 33, 35);
|
||||
--color-error-900: rgb(51, 22, 24);
|
||||
--color-error-950: rgb(25, 11, 12);
|
||||
}
|
||||
1
examples/auth/next-app/app/_css/common.scss
Normal file
1
examples/auth/next-app/app/_css/common.scss
Normal file
@@ -0,0 +1 @@
|
||||
@forward './queries.scss';
|
||||
28
examples/auth/next-app/app/_css/queries.scss
Normal file
28
examples/auth/next-app/app/_css/queries.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
$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;
|
||||
}
|
||||
}
|
||||
241
examples/auth/next-app/app/_css/theme.scss
Normal file
241
examples/auth/next-app/app/_css/theme.scss
Normal file
@@ -0,0 +1,241 @@
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
110
examples/auth/next-app/app/_css/type.scss
Normal file
110
examples/auth/next-app/app/_css/type.scss
Normal file
@@ -0,0 +1,110 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
41
examples/auth/next-app/app/_utilities/getMeUser.ts
Normal file
41
examples/auth/next-app/app/_utilities/getMeUser.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
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_CMS_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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
@import "../../_css/common";
|
||||
|
||||
.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);
|
||||
}
|
||||
152
examples/auth/next-app/app/account/AccountForm/index.tsx
Normal file
152
examples/auth/next-app/app/account/AccountForm/index.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
'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_CMS_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,17 +0,0 @@
|
||||
.form {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.success,
|
||||
.error,
|
||||
.message {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
7
examples/auth/next-app/app/account/index.module.scss
Normal file
7
examples/auth/next-app/app/account/index.module.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.account {
|
||||
margin-bottom: var(--block-padding);
|
||||
}
|
||||
|
||||
.params {
|
||||
margin-top: var(--base);
|
||||
}
|
||||
@@ -1,111 +1,34 @@
|
||||
'use client'
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
|
||||
import { useAuth } from '../_components/Auth'
|
||||
import { Button } from '../_components/Button'
|
||||
import { Gutter } from '../_components/Gutter'
|
||||
import { Input } from '../_components/Input'
|
||||
import classes from './index.module.css'
|
||||
import { RenderParams } from '../_components/RenderParams'
|
||||
import { getMeUser } from '../_utilities/getMeUser'
|
||||
import { AccountForm } from './AccountForm'
|
||||
|
||||
type FormData = {
|
||||
email: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
}
|
||||
import classes from './index.module.scss'
|
||||
|
||||
const Account: React.FC = () => {
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState('')
|
||||
const { user, setUser } = useAuth()
|
||||
const router = useRouter()
|
||||
const params = useSearchParams()
|
||||
const queryMsg = params.get('message')
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
reset,
|
||||
} = useForm<FormData>()
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
if (user) {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_CMS_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()
|
||||
|
||||
// Update the user in auth state with new values
|
||||
setUser(json.doc)
|
||||
|
||||
// Set success message for user
|
||||
setSuccess('Successfully updated account.')
|
||||
|
||||
// Clear any existing errors
|
||||
setError('')
|
||||
} else {
|
||||
setError('There was a problem updating your account.')
|
||||
}
|
||||
}
|
||||
},
|
||||
[user, setUser],
|
||||
)
|
||||
|
||||
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,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
})
|
||||
}
|
||||
}, [user, reset, router])
|
||||
|
||||
useEffect(() => {
|
||||
const success = params.get('success')
|
||||
if (success) {
|
||||
setSuccess(success)
|
||||
}
|
||||
}, [router, params])
|
||||
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>
|
||||
<Gutter className={classes.account}>
|
||||
<RenderParams className={classes.params} />
|
||||
<h1>Account</h1>
|
||||
{queryMsg && <div className={classes.message}>{queryMsg}</div>}
|
||||
{error && <div className={classes.error}>{error}</div>}
|
||||
{success && <div className={classes.success}>{success}</div>}
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
||||
<Input
|
||||
name="email"
|
||||
label="Email Address"
|
||||
required
|
||||
register={register}
|
||||
error={errors.email}
|
||||
/>
|
||||
<Input name="firstName" label="First Name" register={register} error={errors.firstName} />
|
||||
<Input name="lastName" label="Last Name" register={register} error={errors.lastName} />
|
||||
<button type="submit">Update account</button>
|
||||
</form>
|
||||
<Link href="/logout">Log out</Link>
|
||||
<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_CMS_URL}/admin/collections/users`}>
|
||||
login to the admin dashboard
|
||||
</Link>
|
||||
{'.'}
|
||||
</p>
|
||||
<AccountForm />
|
||||
<Button href="/logout" appearance="secondary" label="Log out" />
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
|
||||
export default Account
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
$breakpoint: 1000px;
|
||||
|
||||
:root {
|
||||
--max-width: 1600px;
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-rgb: 255, 255, 255;
|
||||
--block-spacing: 2rem;
|
||||
--gutter-h: 4rem;
|
||||
--base: 1rem;
|
||||
|
||||
@media (max-width: $breakpoint) {
|
||||
--block-spacing: 1rem;
|
||||
--gutter-h: 2rem;
|
||||
--base: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-rgb: 7, 7, 7;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 20px;
|
||||
line-height: 1.5;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
|
||||
@media (max-width: $breakpoint) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: rgb(var(--background-rgb));
|
||||
}
|
||||
|
||||
img {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 4.5rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 2.5rem 0;
|
||||
|
||||
@media (max-width: $breakpoint) {
|
||||
font-size: 3rem;
|
||||
margin: 0 0 1.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 3.5rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 2.5rem 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 2.5rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 2rem 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
@import "../../_css/common";
|
||||
|
||||
.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);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
'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_CMS_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_CMS_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,8 +0,0 @@
|
||||
.form {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@import "../_css/common";
|
||||
|
||||
.createAccount {
|
||||
margin-bottom: var(--block-padding);
|
||||
}
|
||||
@@ -1,92 +1,24 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useAuth } from '../_components/Auth'
|
||||
import { Gutter } from '../_components/Gutter'
|
||||
import { Input } from '../_components/Input'
|
||||
import classes from './index.module.css'
|
||||
import { RenderParams } from '../_components/RenderParams'
|
||||
import { getMeUser } from '../_utilities/getMeUser'
|
||||
import { CreateAccountForm } from './CreateAccountForm'
|
||||
|
||||
type FormData = {
|
||||
email: string
|
||||
password: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
}
|
||||
import classes from './index.module.scss'
|
||||
|
||||
const CreateAccount: React.FC = () => {
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState(false)
|
||||
const { login, create, user } = useAuth()
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<FormData>()
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
try {
|
||||
await create(data as Parameters<typeof create>[0])
|
||||
// Automatically log the user in after creating their account
|
||||
await login({ email: data.email, password: data.password })
|
||||
setSuccess(true)
|
||||
} catch (err: any) {
|
||||
setError(err?.message || 'An error occurred while attempting to create your account.')
|
||||
}
|
||||
},
|
||||
[login, create],
|
||||
)
|
||||
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>
|
||||
{!success && (
|
||||
<React.Fragment>
|
||||
<h1>Create Account</h1>
|
||||
{error && <div className={classes.error}>{error}</div>}
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
||||
<Input
|
||||
name="email"
|
||||
label="Email Address"
|
||||
required
|
||||
register={register}
|
||||
error={errors.email}
|
||||
/>
|
||||
<Input
|
||||
name="password"
|
||||
type="password"
|
||||
label="Password"
|
||||
required
|
||||
register={register}
|
||||
error={errors.password}
|
||||
/>
|
||||
<Input
|
||||
name="firstName"
|
||||
label="First Name"
|
||||
register={register}
|
||||
error={errors.firstName}
|
||||
/>
|
||||
<Input name="lastName" label="Last Name" register={register} error={errors.lastName} />
|
||||
<button type="submit">Create account</button>
|
||||
</form>
|
||||
<p>
|
||||
{'Already have an account? '}
|
||||
<Link href="/login">Login</Link>
|
||||
</p>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{success && (
|
||||
<React.Fragment>
|
||||
<h1>Account created successfully</h1>
|
||||
<p>You are now logged in.</p>
|
||||
<Link href="/account">Go to your account</Link>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<Gutter className={classes.createAccount}>
|
||||
<h1>Create Account</h1>
|
||||
<RenderParams />
|
||||
<CreateAccountForm />
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateAccount
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.page {
|
||||
margin-top: calc(var(--base) * 2);
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import { AuthProvider } from './_components/Auth'
|
||||
import { Header } from './_components/Header'
|
||||
import { AuthProvider } from './_providers/Auth'
|
||||
|
||||
import './app.scss'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
import './_css/app.scss'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Payload Auth + Next.js App Router Example',
|
||||
@@ -16,9 +14,13 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<AuthProvider>
|
||||
<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 />
|
||||
<div className={classes.page}>{children}</div>
|
||||
<main>{children}</main>
|
||||
</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
22
examples/auth/next-app/app/login/LoginForm/index.module.scss
Normal file
22
examples/auth/next-app/app/login/LoginForm/index.module.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
@import "../../_css/common";
|
||||
|
||||
.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);
|
||||
}
|
||||
96
examples/auth/next-app/app/login/LoginForm/index.tsx
Normal file
96
examples/auth/next-app/app/login/LoginForm/index.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
'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_CMS_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,8 +0,0 @@
|
||||
.form {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
9
examples/auth/next-app/app/login/index.module.scss
Normal file
9
examples/auth/next-app/app/login/index.module.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
@import "../_css/common";
|
||||
|
||||
.login {
|
||||
margin-bottom: var(--block-padding);
|
||||
}
|
||||
|
||||
.params {
|
||||
margin-top: var(--base);
|
||||
}
|
||||
@@ -1,85 +1,22 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import Link from 'next/link'
|
||||
import { redirect, useRouter, useSearchParams } from 'next/navigation'
|
||||
|
||||
import { useAuth } from '../_components/Auth'
|
||||
import { Gutter } from '../_components/Gutter'
|
||||
import { Input } from '../_components/Input'
|
||||
import classes from './index.module.css'
|
||||
import { RenderParams } from '../_components/RenderParams'
|
||||
import { getMeUser } from '../_utilities/getMeUser'
|
||||
import { LoginForm } from './LoginForm'
|
||||
|
||||
type FormData = {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
import classes from './index.module.scss'
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const [error, setError] = useState('')
|
||||
const router = useRouter()
|
||||
const params = useSearchParams()
|
||||
const { login, user } = useAuth()
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<FormData>()
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
try {
|
||||
await login(data)
|
||||
router.push('/account')
|
||||
} catch (err: any) {
|
||||
setError(err?.message || 'An error occurred while attempting to login.')
|
||||
}
|
||||
},
|
||||
[login, router],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const unauthorized = params.get('unauthorized')
|
||||
if (unauthorized) {
|
||||
setError(`To visit the ${unauthorized} page, you need to be logged in.`)
|
||||
}
|
||||
}, [params])
|
||||
|
||||
if (user) {
|
||||
redirect('/account')
|
||||
}
|
||||
export default async function Login() {
|
||||
await getMeUser({
|
||||
validUserRedirect: `/account?message=${encodeURIComponent('You are already logged in.')}`,
|
||||
})
|
||||
|
||||
return (
|
||||
<Gutter>
|
||||
<Gutter className={classes.login}>
|
||||
<RenderParams className={classes.params} />
|
||||
<h1>Log in</h1>
|
||||
<p>
|
||||
To log in, use the email <b>demo@payloadcms.com</b> with the password <b>demo</b>.
|
||||
</p>
|
||||
{error && <div className={classes.error}>{error}</div>}
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
||||
<Input
|
||||
name="email"
|
||||
label="Email Address"
|
||||
required
|
||||
register={register}
|
||||
error={errors.email}
|
||||
/>
|
||||
<Input
|
||||
name="password"
|
||||
type="password"
|
||||
label="Password"
|
||||
required
|
||||
register={register}
|
||||
error={errors.password}
|
||||
/>
|
||||
<input type="submit" />
|
||||
</form>
|
||||
<Link href="/create-account">Create an account</Link>
|
||||
<br />
|
||||
<Link href="/recover-password">Recover your password</Link>
|
||||
<LoginForm />
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
|
||||
export default Login
|
||||
|
||||
42
examples/auth/next-app/app/logout/LogoutPage/index.tsx
Normal file
42
examples/auth/next-app/app/logout/LogoutPage/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
'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,4 +0,0 @@
|
||||
.error {
|
||||
color: red;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
3
examples/auth/next-app/app/logout/index.module.scss
Normal file
3
examples/auth/next-app/app/logout/index.module.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.logout {
|
||||
margin-bottom: var(--block-padding);
|
||||
}
|
||||
@@ -1,44 +1,14 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
import React, { Fragment, useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useAuth } from '../_components/Auth'
|
||||
import { Gutter } from '../_components/Gutter'
|
||||
import classes from './index.module.css'
|
||||
import { LogoutPage } from './LogoutPage'
|
||||
|
||||
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 (err: any) {
|
||||
setError(err?.message || 'An error occurred while attempting to logout.')
|
||||
}
|
||||
}
|
||||
|
||||
performLogout()
|
||||
}, [logout])
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export default async function Logout() {
|
||||
return (
|
||||
<Gutter>
|
||||
{success && <h1>{success}</h1>}
|
||||
{error && <div className={classes.error}>{error}</div>}
|
||||
<p>
|
||||
{'What would you like to do next? '}
|
||||
<Fragment>
|
||||
{' To log back in, '}
|
||||
<Link href={`/login`}>click here</Link>
|
||||
{'.'}
|
||||
</Fragment>
|
||||
</p>
|
||||
<Gutter className={classes.logout}>
|
||||
<LogoutPage />
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
|
||||
export default Logout
|
||||
|
||||
@@ -19,20 +19,26 @@ export default function Home() {
|
||||
<Link href="https://nextjs.org/docs/app" target="_blank" rel="noopener noreferrer">
|
||||
App Router
|
||||
</Link>
|
||||
{" made explicitly for Payload's "}
|
||||
<Link href="https://github.com/payloadcms/payload/tree/master/examples/auth/cms">
|
||||
Auth Example
|
||||
{' made explicitly for the '}
|
||||
<Link href="https://github.com/payloadcms/payload/tree/master/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 in both the REST and GraphQL APIs.'}
|
||||
{
|
||||
' strategies in both the REST and GraphQL APIs. To toggle between these APIs, see `_layout.tsx`.'
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
{'Visit the '}
|
||||
<Link href="/login">Login</Link>
|
||||
{' page to start the authentication flow. Once logged in, you will be redirected to the '}
|
||||
<Link href="/account">Account</Link>
|
||||
{` page which is restricted to users only. To toggle APIs, simply toggle the "api" prop between "rest" and "gql" in "_app.tsx".`}
|
||||
<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_CMS_URL}/admin/collections/users`}>
|
||||
login to the admin dashboard
|
||||
</Link>
|
||||
{'.'}
|
||||
</p>
|
||||
</Gutter>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
@import "../../_css/common";
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
'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_CMS_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 your all users, `}
|
||||
<Link href={`${process.env.NEXT_PUBLIC_CMS_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,4 +0,0 @@
|
||||
.error {
|
||||
color: red;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@import "../_css/common";
|
||||
|
||||
.recoverPassword {
|
||||
margin-bottom: var(--block-padding);
|
||||
}
|
||||
@@ -1,74 +1,14 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
import { useAuth } from '../_components/Auth'
|
||||
import { Gutter } from '../_components/Gutter'
|
||||
import { Input } from '../_components/Input'
|
||||
import classes from './index.module.css'
|
||||
import { RecoverPasswordForm } from './RecoverPasswordForm'
|
||||
|
||||
type FormData = {
|
||||
email: string
|
||||
}
|
||||
|
||||
const RecoverPassword: React.FC = () => {
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState(false)
|
||||
const { forgotPassword } = useAuth()
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<FormData>()
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
try {
|
||||
const user = await forgotPassword(data as Parameters<typeof forgotPassword>[0])
|
||||
|
||||
if (user) {
|
||||
setSuccess(true)
|
||||
setError('')
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err?.message || 'An error occurred while attempting to recover password.')
|
||||
}
|
||||
},
|
||||
[forgotPassword],
|
||||
)
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export default async function RecoverPassword() {
|
||||
return (
|
||||
<Gutter>
|
||||
{!success && (
|
||||
<React.Fragment>
|
||||
<h1>Recover Password</h1>
|
||||
<p>
|
||||
Please enter your email below. You will receive an email message with instructions on
|
||||
how to reset your password.
|
||||
</p>
|
||||
{error && <div className={classes.error}>{error}</div>}
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Input
|
||||
name="email"
|
||||
label="Email Address"
|
||||
required
|
||||
register={register}
|
||||
error={errors.email}
|
||||
/>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</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 className={classes.recoverPassword}>
|
||||
<RecoverPasswordForm />
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecoverPassword
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
@import "../../_css/common";
|
||||
|
||||
.form {
|
||||
width: 66.66%;
|
||||
|
||||
@include mid-break {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin-top: var(--base);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
'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_CMS_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,4 +0,0 @@
|
||||
.error {
|
||||
color: red;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.resetPassword {
|
||||
margin-bottom: var(--block-padding);
|
||||
}
|
||||
@@ -1,75 +1,16 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
|
||||
import { useAuth } from '../_components/Auth'
|
||||
import { Gutter } from '../_components/Gutter'
|
||||
import { Input } from '../_components/Input'
|
||||
import classes from './index.module.css'
|
||||
import { ResetPasswordForm } from './ResetPasswordForm'
|
||||
|
||||
type FormData = {
|
||||
password: string
|
||||
token: string | null
|
||||
}
|
||||
|
||||
const ResetPassword: React.FC = () => {
|
||||
const [error, setError] = useState('')
|
||||
const { login, resetPassword } = useAuth()
|
||||
const router = useRouter()
|
||||
const params = useSearchParams()
|
||||
const token = params.get('token')
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
reset,
|
||||
} = useForm<FormData>()
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
try {
|
||||
const user = await resetPassword(data as Parameters<typeof resetPassword>[0])
|
||||
|
||||
if (user) {
|
||||
// Automatically log the user in after they successfully reset password
|
||||
// Then redirect them to /account with success message in URL
|
||||
await login({ email: user.email, password: data.password })
|
||||
router.push('/account?success=Password reset successfully.')
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err?.message || 'An error occurred while attempting to reset password.')
|
||||
}
|
||||
},
|
||||
[router, login, resetPassword],
|
||||
)
|
||||
|
||||
// When Next.js populates token within router, reset form with new token value
|
||||
useEffect(() => {
|
||||
reset({ token })
|
||||
}, [reset, token])
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export default async function ResetPassword() {
|
||||
return (
|
||||
<Gutter>
|
||||
<Gutter className={classes.resetPassword}>
|
||||
<h1>Reset Password</h1>
|
||||
<p>Please enter a new password below.</p>
|
||||
{error && <div className={classes.error}>{error}</div>}
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Input
|
||||
name="password"
|
||||
type="password"
|
||||
label="New Password"
|
||||
required
|
||||
register={register}
|
||||
error={errors.password}
|
||||
/>
|
||||
<input type="hidden" {...register('token')} />
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
<ResetPasswordForm />
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResetPassword
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Payload Auth Example Front-End
|
||||
|
||||
This is a [Next.js](https://nextjs.org) app using the [Pages Router](https://nextjs.org/docs/pages). It was made explicitly for Payload's [Auth Example](https://github.com/payloadcms/payload/tree/master/examples/auth/cms).
|
||||
This is a [Payload](https://payloadcms.com) + [Next.js](https://nextjs.org) app using the [Pages Router](https://nextjs.org/docs/pages) made explicitly for the [Payload Auth Example](https://github.com/payloadcms/payload/tree/master/examples/auth). It 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/pages), check out the official [App Router Example](https://github.com/payloadcms/payload/tree/master/examples/auth/next-app).
|
||||
|
||||
@@ -8,7 +8,7 @@ This is a [Next.js](https://nextjs.org) app using the [Pages Router](https://nex
|
||||
|
||||
### Payload
|
||||
|
||||
First you'll need a running [Payload](https://github.com/payloadcms/payload) app. If you have not done so already, open up the `cms` folder and follow the setup instructions. Take note of your `serverURL`, you'll need this in the next step.
|
||||
First you'll need a running Payload app. If you have not done so already, clone down the [`cms`](../cms) folder and follow the setup instructions there to get it up and running. This will provide all the necessary APIs that your Next.js app will be using for authentication.
|
||||
|
||||
### Next.js
|
||||
|
||||
@@ -18,20 +18,24 @@ First you'll need a running [Payload](https://github.com/payloadcms/payload) app
|
||||
4. `yarn dev` or `npm run dev` to start the server
|
||||
5. `open http://localhost:3001` to see the result
|
||||
|
||||
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Auth Example](https://github.com/payloadcms/payload/tree/master/examples/auth/cms) for full details.
|
||||
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/master/examples/auth) for full details.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about PayloadCMS and Next.js, take a look at the following resources:
|
||||
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!
|
||||
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/master/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).
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
@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);
|
||||
}
|
||||
73
examples/auth/next-pages/src/components/Button/index.tsx
Normal file
73
examples/auth/next-pages/src/components/Button/index.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
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,7 +1,7 @@
|
||||
.gutter {
|
||||
max-width: var(--max-width);
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
max-width: 1920px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.gutterLeft {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
@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;
|
||||
}
|
||||
36
examples/auth/next-pages/src/components/Header/Nav/index.tsx
Normal file
36
examples/auth/next-pages/src/components/Header/Nav/index.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
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,16 +1,22 @@
|
||||
@use '../../css/queries.scss' as *;
|
||||
|
||||
.header {
|
||||
padding: var(--base) 0;
|
||||
z-index: var(--header-z-index);
|
||||
}
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: calc(var(--base) / 2) var(--base);
|
||||
}
|
||||
|
||||
.nav {
|
||||
a {
|
||||
text-decoration: none;
|
||||
margin-left: var(--base);
|
||||
.logo {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
:global([data-theme="light"]) {
|
||||
.logo {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import React, { Fragment } from 'react'
|
||||
import React from 'react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useAuth } from '../Auth'
|
||||
import { Gutter } from '../Gutter'
|
||||
import { HeaderNav } from './Nav'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export const Header: React.FC = () => {
|
||||
const { user } = useAuth()
|
||||
|
||||
return (
|
||||
<header className={classes.header}>
|
||||
<Gutter className={classes.wrap}>
|
||||
@@ -27,20 +25,7 @@ export const Header: React.FC = () => {
|
||||
/>
|
||||
</picture>
|
||||
</Link>
|
||||
<nav className={classes.nav}>
|
||||
{!user && (
|
||||
<Fragment>
|
||||
<Link href="/login">Login</Link>
|
||||
<Link href="/create-account">Create Account</Link>
|
||||
</Fragment>
|
||||
)}
|
||||
{user && (
|
||||
<Fragment>
|
||||
<Link href="/account">Account</Link>
|
||||
<Link href="/logout">Logout</Link>
|
||||
</Fragment>
|
||||
)}
|
||||
</nav>
|
||||
<HeaderNav />
|
||||
</Gutter>
|
||||
</header>
|
||||
)
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
.input {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.input input {
|
||||
width: 100%;
|
||||
font-family: system-ui;
|
||||
border-radius: 0;
|
||||
box-shadow: 0;
|
||||
border: 1px solid #d8d8d8;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-top: 5px;
|
||||
color: red;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
@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,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { FieldValues, UseFormRegister } from 'react-hook-form'
|
||||
import { FieldValues, UseFormRegister, Validate } from 'react-hook-form'
|
||||
|
||||
import classes from './index.module.css'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type Props = {
|
||||
name: string
|
||||
@@ -9,7 +9,8 @@ type Props = {
|
||||
register: UseFormRegister<FieldValues & any>
|
||||
required?: boolean
|
||||
error: any
|
||||
type?: 'text' | 'number' | 'password'
|
||||
type?: 'text' | 'number' | 'password' | 'email'
|
||||
validate?: (value: string) => boolean | string
|
||||
}
|
||||
|
||||
export const Input: React.FC<Props> = ({
|
||||
@@ -19,14 +20,36 @@ export const Input: React.FC<Props> = ({
|
||||
register,
|
||||
error,
|
||||
type = 'text',
|
||||
validate,
|
||||
}) => {
|
||||
return (
|
||||
<div className={classes.input}>
|
||||
<div className={classes.inputWrap}>
|
||||
<label htmlFor="name" className={classes.label}>
|
||||
{label}
|
||||
{`${label} ${required ? '*' : ''}`}
|
||||
</label>
|
||||
<input {...{ type }} {...register(name, { required })} />
|
||||
{error && <div className={classes.error}>This field is required</div>}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
@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);
|
||||
}
|
||||
}
|
||||
33
examples/auth/next-pages/src/components/Message/index.tsx
Normal file
33
examples/auth/next-pages/src/components/Message/index.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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
|
||||
}
|
||||
117
examples/auth/next-pages/src/css/app.scss
Normal file
117
examples/auth/next-pages/src/css/app.scss
Normal file
@@ -0,0 +1,117 @@
|
||||
@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) * .75) 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: var(--base);
|
||||
margin: 0 0 var(--base);
|
||||
}
|
||||
|
||||
a {
|
||||
color: currentColor;
|
||||
|
||||
&:focus {
|
||||
opacity: .8;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: .7;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
83
examples/auth/next-pages/src/css/colors.scss
Normal file
83
examples/auth/next-pages/src/css/colors.scss
Normal file
@@ -0,0 +1,83 @@
|
||||
: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(247, 255, 251);
|
||||
--color-success-100: rgb(240, 255, 247);
|
||||
--color-success-150: rgb(232, 255, 243);
|
||||
--color-success-200: rgb(224, 255, 239);
|
||||
--color-success-250: rgb(217, 255, 235);
|
||||
--color-success-300: rgb(209, 255, 230);
|
||||
--color-success-350: rgb(201, 255, 226);
|
||||
--color-success-400: rgb(193, 255, 222);
|
||||
--color-success-450: rgb(186, 255, 218);
|
||||
--color-success-500: rgb(178, 255, 214);
|
||||
--color-success-550: rgb(160, 230, 193);
|
||||
--color-success-600: rgb(142, 204, 171);
|
||||
--color-success-650: rgb(125, 179, 150);
|
||||
--color-success-700: rgb(107, 153, 128);
|
||||
--color-success-750: rgb(89, 128, 107);
|
||||
--color-success-800: rgb(71, 102, 86);
|
||||
--color-success-850: rgb(53, 77, 64);
|
||||
--color-success-900: rgb(36, 51, 43);
|
||||
--color-success-950: rgb(18, 25, 21);
|
||||
|
||||
--color-warning-50: rgb(255, 255, 246);
|
||||
--color-warning-100: rgb(255, 255, 237);
|
||||
--color-warning-150: rgb(254, 255, 228);
|
||||
--color-warning-200: rgb(254, 255, 219);
|
||||
--color-warning-250: rgb(254, 255, 210);
|
||||
--color-warning-300: rgb(254, 255, 200);
|
||||
--color-warning-350: rgb(254, 255, 191);
|
||||
--color-warning-400: rgb(253, 255, 182);
|
||||
--color-warning-450: rgb(253, 255, 173);
|
||||
--color-warning-500: rgb(253, 255, 164);
|
||||
--color-warning-550: rgb(228, 230, 148);
|
||||
--color-warning-600: rgb(202, 204, 131);
|
||||
--color-warning-650: rgb(177, 179, 115);
|
||||
--color-warning-700: rgb(152, 153, 98);
|
||||
--color-warning-750: rgb(127, 128, 82);
|
||||
--color-warning-800: rgb(101, 102, 66);
|
||||
--color-warning-850: rgb(76, 77, 49);
|
||||
--color-warning-900: rgb(51, 51, 33);
|
||||
--color-warning-950: rgb(25, 25, 16);
|
||||
|
||||
--color-error-50: rgb(255, 241, 241);
|
||||
--color-error-100: rgb(255, 226, 228);
|
||||
--color-error-150: rgb(255, 212, 214);
|
||||
--color-error-200: rgb(255, 197, 200);
|
||||
--color-error-250: rgb(255, 183, 187);
|
||||
--color-error-300: rgb(255, 169, 173);
|
||||
--color-error-350: rgb(255, 154, 159);
|
||||
--color-error-400: rgb(255, 140, 145);
|
||||
--color-error-450: rgb(255, 125, 132);
|
||||
--color-error-500: rgb(255, 111, 118);
|
||||
--color-error-550: rgb(230, 100, 106);
|
||||
--color-error-600: rgb(204, 89, 94);
|
||||
--color-error-650: rgb(179, 78, 83);
|
||||
--color-error-700: rgb(153, 67, 71);
|
||||
--color-error-750: rgb(128, 56, 59);
|
||||
--color-error-800: rgb(102, 44, 47);
|
||||
--color-error-850: rgb(77, 33, 35);
|
||||
--color-error-900: rgb(51, 22, 24);
|
||||
--color-error-950: rgb(25, 11, 12);
|
||||
}
|
||||
1
examples/auth/next-pages/src/css/common.scss
Normal file
1
examples/auth/next-pages/src/css/common.scss
Normal file
@@ -0,0 +1 @@
|
||||
@forward './queries.scss';
|
||||
28
examples/auth/next-pages/src/css/queries.scss
Normal file
28
examples/auth/next-pages/src/css/queries.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
$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;
|
||||
}
|
||||
}
|
||||
241
examples/auth/next-pages/src/css/theme.scss
Normal file
241
examples/auth/next-pages/src/css/theme.scss
Normal file
@@ -0,0 +1,241 @@
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
110
examples/auth/next-pages/src/css/type.scss
Normal file
110
examples/auth/next-pages/src/css/type.scss
Normal file
@@ -0,0 +1,110 @@
|
||||
@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,26 +1,24 @@
|
||||
import type { AppProps } from 'next/app'
|
||||
|
||||
import { AuthProvider } from '../components/Auth'
|
||||
import { Header } from '../components/Header'
|
||||
import { AuthProvider } from '../providers/Auth'
|
||||
|
||||
import './app.scss'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
import '../css/app.scss'
|
||||
|
||||
export default function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
// The `AuthProvider` can be used with either REST or GraphQL APIs
|
||||
// Just change the `api` prop to "graphql" or "rest", that's it!
|
||||
<AuthProvider api="rest">
|
||||
<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 />
|
||||
<div className={classes.page}>
|
||||
{/* typescript flags this `@ts-expect-error` declaration as unneeded, but types are breaking the build process
|
||||
{/* 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} />
|
||||
</div>
|
||||
{/* @ts-expect-error */}
|
||||
<Component {...pageProps} />
|
||||
</AuthProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
.form {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.success,
|
||||
.error,
|
||||
.message {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
32
examples/auth/next-pages/src/pages/account/index.module.scss
Normal file
32
examples/auth/next-pages/src/pages/account/index.module.scss
Normal file
@@ -0,0 +1,32 @@
|
||||
@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,32 +1,42 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
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 { useAuth } from '../../components/Auth'
|
||||
import { Button } from '../../components/Button'
|
||||
import { Gutter } from '../../components/Gutter'
|
||||
import { Input } from '../../components/Input'
|
||||
import classes from './index.module.css'
|
||||
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
|
||||
firstName: string
|
||||
lastName: 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 },
|
||||
formState: { errors, isLoading },
|
||||
reset,
|
||||
watch,
|
||||
} = useForm<FormData>()
|
||||
|
||||
const password = useRef({})
|
||||
password.current = watch('password', '')
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
if (user) {
|
||||
@@ -42,21 +52,22 @@ const Account: React.FC = () => {
|
||||
|
||||
if (response.ok) {
|
||||
const json = await response.json()
|
||||
|
||||
// Update the user in auth state with new values
|
||||
setUser(json.doc)
|
||||
|
||||
// Set success message for user
|
||||
setSuccess('Successfully updated account.')
|
||||
|
||||
// Clear any existing errors
|
||||
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],
|
||||
[user, setUser, reset],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -68,37 +79,87 @@ const Account: React.FC = () => {
|
||||
if (user) {
|
||||
reset({
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
password: '',
|
||||
passwordConfirm: '',
|
||||
})
|
||||
}
|
||||
}, [user, reset, router])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof router.query.success === 'string') {
|
||||
setSuccess(router.query.success)
|
||||
}
|
||||
}, [router])
|
||||
}, [user, router, reset, changePassword])
|
||||
|
||||
return (
|
||||
<Gutter>
|
||||
<Gutter className={classes.account}>
|
||||
<RenderParams className={classes.params} />
|
||||
<h1>Account</h1>
|
||||
{router.query.message && <div className={classes.message}>{router.query.message}</div>}
|
||||
{error && <div className={classes.error}>{error}</div>}
|
||||
{success && <div className={classes.success}>{success}</div>}
|
||||
<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_CMS_URL}/admin/collections/users`}>
|
||||
login to the admin dashboard
|
||||
</Link>
|
||||
{'.'}
|
||||
</p>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
||||
<Input
|
||||
name="email"
|
||||
label="Email Address"
|
||||
required
|
||||
register={register}
|
||||
error={errors.email}
|
||||
<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"
|
||||
/>
|
||||
<Input name="firstName" label="First Name" register={register} error={errors.firstName} />
|
||||
<Input name="lastName" label="Last Name" register={register} error={errors.lastName} />
|
||||
<button type="submit">Update account</button>
|
||||
</form>
|
||||
<Link href="/logout">Log out</Link>
|
||||
<Button href="/logout" appearance="secondary" label="Log out" />
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
$breakpoint: 1000px;
|
||||
|
||||
:root {
|
||||
--max-width: 1600px;
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-rgb: 255, 255, 255;
|
||||
--block-spacing: 2rem;
|
||||
--gutter-h: 4rem;
|
||||
--base: 1rem;
|
||||
|
||||
@media (max-width: $breakpoint) {
|
||||
--block-spacing: 1rem;
|
||||
--gutter-h: 2rem;
|
||||
--base: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-rgb: 7, 7, 7;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 20px;
|
||||
line-height: 1.5;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
|
||||
@media (max-width: $breakpoint) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: rgb(var(--background-rgb));
|
||||
}
|
||||
|
||||
img {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 4.5rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 2.5rem 0;
|
||||
|
||||
@media (max-width: $breakpoint) {
|
||||
font-size: 3rem;
|
||||
margin: 0 0 1.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 3.5rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 2.5rem 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 2.5rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 2rem 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
.form {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
@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,88 +1,125 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
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 { useAuth } from '../../components/Auth'
|
||||
import { Button } from '../../components/Button'
|
||||
import { Gutter } from '../../components/Gutter'
|
||||
import { Input } from '../../components/Input'
|
||||
import classes from './index.module.css'
|
||||
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
|
||||
firstName: string
|
||||
lastName: string
|
||||
passwordConfirm: string
|
||||
}
|
||||
|
||||
const CreateAccount: React.FC = () => {
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState(false)
|
||||
const { login, create, user } = useAuth()
|
||||
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_CMS_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 create(data as Parameters<typeof create>[0])
|
||||
// Automatically log the user in after creating their account
|
||||
await login({ email: data.email, password: data.password })
|
||||
setSuccess(true)
|
||||
} catch (err) {
|
||||
setError(err?.message || 'An error occurred while attempting to create your account.')
|
||||
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, create],
|
||||
[login, router, searchParams],
|
||||
)
|
||||
|
||||
return (
|
||||
<Gutter>
|
||||
{!success && (
|
||||
<React.Fragment>
|
||||
<h1>Create Account</h1>
|
||||
{error && <div className={classes.error}>{error}</div>}
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
||||
<Input
|
||||
name="email"
|
||||
label="Email Address"
|
||||
required
|
||||
register={register}
|
||||
error={errors.email}
|
||||
/>
|
||||
<Input
|
||||
name="password"
|
||||
type="password"
|
||||
label="Password"
|
||||
required
|
||||
register={register}
|
||||
error={errors.password}
|
||||
/>
|
||||
<Input
|
||||
name="firstName"
|
||||
label="First Name"
|
||||
register={register}
|
||||
error={errors.firstName}
|
||||
/>
|
||||
<Input name="lastName" label="Last Name" register={register} error={errors.lastName} />
|
||||
<button type="submit">Create account</button>
|
||||
</form>
|
||||
<p>
|
||||
{'Already have an account? '}
|
||||
<Link href="/login">Login</Link>
|
||||
</p>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{success && (
|
||||
<React.Fragment>
|
||||
<h1>Account created successfully</h1>
|
||||
<p>You are now logged in.</p>
|
||||
<Link href="/account">Go to your account</Link>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<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_CMS_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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.page {
|
||||
margin-top: calc(var(--base) * 2);
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { Gutter } from '../components/Gutter'
|
||||
|
||||
const Home: React.FC = () => {
|
||||
export default function Home() {
|
||||
return (
|
||||
<Gutter>
|
||||
<h1>Payload Auth Example</h1>
|
||||
@@ -13,40 +12,34 @@ const Home: React.FC = () => {
|
||||
Payload
|
||||
</Link>
|
||||
{' + '}
|
||||
<Link href="https://nextjs.org/" target="_blank" rel="noopener noreferrer">
|
||||
<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 Payload's "}
|
||||
<Link
|
||||
href="https://github.com/payloadcms/payload/tree/master/examples/auth/cms"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Auth Example
|
||||
{' made explicitly for the '}
|
||||
<Link href="https://github.com/payloadcms/payload/tree/master/examples/auth">
|
||||
Payload Auth Example
|
||||
</Link>
|
||||
{". This example demonstrates how to implement Payload's "}
|
||||
<Link
|
||||
href="https://payloadcms.com/docs/authentication/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Authentication
|
||||
</Link>
|
||||
{' strategies in both the REST and GraphQL APIs.'}
|
||||
<Link href="https://payloadcms.com/docs/authentication/overview">Authentication</Link>
|
||||
{
|
||||
' strategies in both the REST and GraphQL APIs. To toggle between these APIs, see `_app.tsx`.'
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
{'Visit the '}
|
||||
<Link href="/login">Login</Link>
|
||||
{' page to start the authentication flow. Once logged in, you will be redirected to the '}
|
||||
<Link href="/account">Account</Link>
|
||||
{` page which is restricted to users only. To toggle APIs, simply toggle the "api" prop between "rest" and "gql" in "_app.tsx".`}
|
||||
<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_CMS_URL}/admin/collections/users`}>
|
||||
login to the admin dashboard
|
||||
</Link>
|
||||
{'.'}
|
||||
</p>
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
.form {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
30
examples/auth/next-pages/src/pages/login/index.module.scss
Normal file
30
examples/auth/next-pages/src/pages/login/index.module.scss
Normal file
@@ -0,0 +1,30 @@
|
||||
@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,12 +1,16 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import React, { useCallback, useMemo, useRef } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useAuth } from '../../components/Auth'
|
||||
import { Button } from '../../components/Button'
|
||||
import { Gutter } from '../../components/Gutter'
|
||||
import { Input } from '../../components/Input'
|
||||
import classes from './index.module.css'
|
||||
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
|
||||
@@ -14,52 +18,61 @@ type FormData = {
|
||||
}
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const [error, setError] = useState('')
|
||||
const router = useRouter()
|
||||
const { login, user } = useAuth()
|
||||
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 },
|
||||
} = useForm<FormData>()
|
||||
formState: { errors, isLoading },
|
||||
} = useForm<FormData>({
|
||||
defaultValues: {
|
||||
email: 'demo@payloadcms.com',
|
||||
password: 'demo',
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
try {
|
||||
await login(data)
|
||||
router.push('/account')
|
||||
} catch (err) {
|
||||
setError(err?.message || 'An error occurred while attempting to login.')
|
||||
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],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (router.query.unauthorized) {
|
||||
setError(`To visit the ${router.query.unauthorized} page, you need to be logged in.`)
|
||||
}
|
||||
}, [router])
|
||||
|
||||
if (user) {
|
||||
router.push('/account')
|
||||
}
|
||||
|
||||
return (
|
||||
<Gutter>
|
||||
<Gutter className={classes.login}>
|
||||
<RenderParams className={classes.params} />
|
||||
<h1>Log in</h1>
|
||||
<p>
|
||||
To log in, use the email <b>demo@payloadcms.com</b> with the password <b>demo</b>.
|
||||
</p>
|
||||
{error && <div className={classes.error}>{error}</div>}
|
||||
<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_CMS_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"
|
||||
@@ -69,11 +82,19 @@ const Login: React.FC = () => {
|
||||
register={register}
|
||||
error={errors.password}
|
||||
/>
|
||||
<input type="submit" />
|
||||
<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>
|
||||
<Link href="/create-account">Create an account</Link>
|
||||
<br />
|
||||
<Link href="/recover-password">Recover your password</Link>
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.error {
|
||||
color: red;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.logout {
|
||||
margin-bottom: var(--block-padding);
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { Fragment, useEffect, useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useAuth } from '../../components/Auth'
|
||||
import { Gutter } from '../../components/Gutter'
|
||||
import classes from './index.module.css'
|
||||
import { useAuth } from '../../providers/Auth'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
const Logout: React.FC = () => {
|
||||
const { logout } = useAuth()
|
||||
@@ -15,8 +16,8 @@ const Logout: React.FC = () => {
|
||||
try {
|
||||
await logout()
|
||||
setSuccess('Logged out successfully.')
|
||||
} catch (err) {
|
||||
setError(err?.message || 'An error occurred while attempting to logout.')
|
||||
} catch (_) {
|
||||
setError('You are already logged out.')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,17 +25,19 @@ const Logout: React.FC = () => {
|
||||
}, [logout])
|
||||
|
||||
return (
|
||||
<Gutter>
|
||||
{success && <h1>{success}</h1>}
|
||||
{error && <div className={classes.error}>{error}</div>}
|
||||
<p>
|
||||
{'What would you like to do next? '}
|
||||
<Fragment>
|
||||
{' To log back in, '}
|
||||
<Link href={`/login`}>click here</Link>
|
||||
{'.'}
|
||||
</Fragment>
|
||||
</p>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.error {
|
||||
color: red;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
@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,10 +1,13 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useAuth } from '../../components/Auth'
|
||||
import { Button } from '../../components/Button'
|
||||
import { Gutter } from '../../components/Gutter'
|
||||
import { Input } from '../../components/Input'
|
||||
import classes from './index.module.css'
|
||||
import { Message } from '../../components/Message'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type FormData = {
|
||||
email: string
|
||||
@@ -13,7 +16,6 @@ type FormData = {
|
||||
const RecoverPassword: React.FC = () => {
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState(false)
|
||||
const { forgotPassword } = useAuth()
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -21,42 +23,57 @@ const RecoverPassword: React.FC = () => {
|
||||
formState: { errors },
|
||||
} = useForm<FormData>()
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
try {
|
||||
const user = await forgotPassword(data as Parameters<typeof forgotPassword>[0])
|
||||
const onSubmit = useCallback(async (data: FormData) => {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/forgot-password`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (user) {
|
||||
setSuccess(true)
|
||||
setError('')
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err?.message || 'An error occurred while attempting to recover password.')
|
||||
}
|
||||
},
|
||||
[forgotPassword],
|
||||
)
|
||||
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>
|
||||
<Gutter className={classes.recoverPassword}>
|
||||
{!success && (
|
||||
<React.Fragment>
|
||||
<h1>Recover Password</h1>
|
||||
<p>
|
||||
Please enter your email below. You will receive an email message with instructions on
|
||||
how to reset your password.
|
||||
</p>
|
||||
{error && <div className={classes.error}>{error}</div>}
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Input
|
||||
name="email"
|
||||
label="Email Address"
|
||||
required
|
||||
register={register}
|
||||
error={errors.email}
|
||||
/>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
<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 your all users, `}
|
||||
<Link href={`${process.env.NEXT_PUBLIC_CMS_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 && (
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.error {
|
||||
color: red;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user