chore: updates auth example (#3026)
This commit is contained in:
@@ -1,8 +1,11 @@
|
|||||||
# Payload Auth Example
|
# 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
|
## Quick Start
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Payload Auth Example Front-End
|
# 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).
|
> 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
|
### 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
|
### 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
|
4. `yarn dev` or `npm run dev` to start the server
|
||||||
5. `open http://localhost:3001` to see the result
|
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
|
## 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).
|
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.
|
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 {
|
.gutter {
|
||||||
max-width: var(--max-width);
|
max-width: 1920px;
|
||||||
width: 100%;
|
margin-left: auto;
|
||||||
margin: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gutterLeft {
|
.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 {
|
.header {
|
||||||
padding: var(--base) 0;
|
padding: var(--base) 0;
|
||||||
}
|
}
|
||||||
@@ -5,28 +7,16 @@
|
|||||||
.wrap {
|
.wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: calc(var(--base) / 2);
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
gap: calc(var(--base) / 2) var(--base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
flex-shrink: 0;
|
width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav {
|
:global([data-theme="light"]) {
|
||||||
display: flex;
|
.logo {
|
||||||
align-items: center;
|
filter: invert(1);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Image from 'next/image'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
import { Gutter } from '../Gutter'
|
import { Gutter } from '../Gutter'
|
||||||
import { HeaderClient } from './index.client'
|
import { HeaderNav } from './Nav'
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
import classes from './index.module.scss'
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ export function Header() {
|
|||||||
/>
|
/>
|
||||||
</picture>
|
</picture>
|
||||||
</Link>
|
</Link>
|
||||||
<HeaderClient />
|
<HeaderNav />
|
||||||
</Gutter>
|
</Gutter>
|
||||||
</header>
|
</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 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 = {
|
type Props = {
|
||||||
name: string
|
name: string
|
||||||
@@ -9,7 +9,8 @@ type Props = {
|
|||||||
register: UseFormRegister<FieldValues & any>
|
register: UseFormRegister<FieldValues & any>
|
||||||
required?: boolean
|
required?: boolean
|
||||||
error: any
|
error: any
|
||||||
type?: 'text' | 'number' | 'password'
|
type?: 'text' | 'number' | 'password' | 'email'
|
||||||
|
validate?: (value: string) => boolean | string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Input: React.FC<Props> = ({
|
export const Input: React.FC<Props> = ({
|
||||||
@@ -19,14 +20,36 @@ export const Input: React.FC<Props> = ({
|
|||||||
register,
|
register,
|
||||||
error,
|
error,
|
||||||
type = 'text',
|
type = 'text',
|
||||||
|
validate,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={classes.input}>
|
<div className={classes.inputWrap}>
|
||||||
<label htmlFor="name" className={classes.label}>
|
<label htmlFor="name" className={classes.label}>
|
||||||
{label}
|
{`${label} ${required ? '*' : ''}`}
|
||||||
</label>
|
</label>
|
||||||
<input {...{ type }} {...register(name, { required })} />
|
<input
|
||||||
{error && <div className={classes.error}>This field is required</div>}
|
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>
|
</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 from 'react'
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
|
||||||
import { useForm } from 'react-hook-form'
|
|
||||||
import Link from 'next/link'
|
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 { Gutter } from '../_components/Gutter'
|
||||||
import { Input } from '../_components/Input'
|
import { RenderParams } from '../_components/RenderParams'
|
||||||
import classes from './index.module.css'
|
import { getMeUser } from '../_utilities/getMeUser'
|
||||||
|
import { AccountForm } from './AccountForm'
|
||||||
|
|
||||||
type FormData = {
|
import classes from './index.module.scss'
|
||||||
email: string
|
|
||||||
firstName: string
|
|
||||||
lastName: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Account: React.FC = () => {
|
export default async function Account() {
|
||||||
const [error, setError] = useState('')
|
await getMeUser({
|
||||||
const [success, setSuccess] = useState('')
|
nullUserRedirect: `/login?error=${encodeURIComponent(
|
||||||
const { user, setUser } = useAuth()
|
'You must be logged in to access your account.',
|
||||||
const router = useRouter()
|
)}&redirect=${encodeURIComponent('/account')}`,
|
||||||
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])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Gutter>
|
<Gutter className={classes.account}>
|
||||||
|
<RenderParams className={classes.params} />
|
||||||
<h1>Account</h1>
|
<h1>Account</h1>
|
||||||
{queryMsg && <div className={classes.message}>{queryMsg}</div>}
|
<p>
|
||||||
{error && <div className={classes.error}>{error}</div>}
|
{`This is your account dashboard. Here you can update your account information and more. To manage all users, `}
|
||||||
{success && <div className={classes.success}>{success}</div>}
|
<Link href={`${process.env.NEXT_PUBLIC_CMS_URL}/admin/collections/users`}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
login to the admin dashboard
|
||||||
<Input
|
</Link>
|
||||||
name="email"
|
{'.'}
|
||||||
label="Email Address"
|
</p>
|
||||||
required
|
<AccountForm />
|
||||||
register={register}
|
<Button href="/logout" appearance="secondary" label="Log out" />
|
||||||
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>
|
|
||||||
</Gutter>
|
</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 { Gutter } from '../_components/Gutter'
|
||||||
import { Input } from '../_components/Input'
|
import { RenderParams } from '../_components/RenderParams'
|
||||||
import classes from './index.module.css'
|
import { getMeUser } from '../_utilities/getMeUser'
|
||||||
|
import { CreateAccountForm } from './CreateAccountForm'
|
||||||
|
|
||||||
type FormData = {
|
import classes from './index.module.scss'
|
||||||
email: string
|
|
||||||
password: string
|
|
||||||
firstName: string
|
|
||||||
lastName: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const CreateAccount: React.FC = () => {
|
export default async function CreateAccount() {
|
||||||
const [error, setError] = useState('')
|
await getMeUser({
|
||||||
const [success, setSuccess] = useState(false)
|
validUserRedirect: `/account?message=${encodeURIComponent(
|
||||||
const { login, create, user } = useAuth()
|
'Cannot create a new account while logged in, please log out and try again.',
|
||||||
|
)}`,
|
||||||
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],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Gutter>
|
<Gutter className={classes.createAccount}>
|
||||||
{!success && (
|
|
||||||
<React.Fragment>
|
|
||||||
<h1>Create Account</h1>
|
<h1>Create Account</h1>
|
||||||
{error && <div className={classes.error}>{error}</div>}
|
<RenderParams />
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
<CreateAccountForm />
|
||||||
<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>
|
</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 { Header } from './_components/Header'
|
||||||
|
import { AuthProvider } from './_providers/Auth'
|
||||||
|
|
||||||
import './app.scss'
|
import './_css/app.scss'
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Payload Auth + Next.js App Router Example',
|
title: 'Payload Auth + Next.js App Router Example',
|
||||||
@@ -16,9 +14,13 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body>
|
<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 />
|
<Header />
|
||||||
<div className={classes.page}>{children}</div>
|
<main>{children}</main>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 { Gutter } from '../_components/Gutter'
|
||||||
import { Input } from '../_components/Input'
|
import { RenderParams } from '../_components/RenderParams'
|
||||||
import classes from './index.module.css'
|
import { getMeUser } from '../_utilities/getMeUser'
|
||||||
|
import { LoginForm } from './LoginForm'
|
||||||
|
|
||||||
type FormData = {
|
import classes from './index.module.scss'
|
||||||
email: string
|
|
||||||
password: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Login: React.FC = () => {
|
export default async function Login() {
|
||||||
const [error, setError] = useState('')
|
await getMeUser({
|
||||||
const router = useRouter()
|
validUserRedirect: `/account?message=${encodeURIComponent('You are already logged in.')}`,
|
||||||
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')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Gutter>
|
<Gutter className={classes.login}>
|
||||||
|
<RenderParams className={classes.params} />
|
||||||
<h1>Log in</h1>
|
<h1>Log in</h1>
|
||||||
<p>
|
<LoginForm />
|
||||||
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>
|
|
||||||
</Gutter>
|
</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 { Gutter } from '../_components/Gutter'
|
||||||
import classes from './index.module.css'
|
import { LogoutPage } from './LogoutPage'
|
||||||
|
|
||||||
const Logout: React.FC = () => {
|
import classes from './index.module.scss'
|
||||||
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])
|
|
||||||
|
|
||||||
|
export default async function Logout() {
|
||||||
return (
|
return (
|
||||||
<Gutter>
|
<Gutter className={classes.logout}>
|
||||||
{success && <h1>{success}</h1>}
|
<LogoutPage />
|
||||||
{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>
|
</Gutter>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Logout
|
|
||||||
|
|||||||
@@ -19,20 +19,26 @@ export default function Home() {
|
|||||||
<Link href="https://nextjs.org/docs/app" target="_blank" rel="noopener noreferrer">
|
<Link href="https://nextjs.org/docs/app" target="_blank" rel="noopener noreferrer">
|
||||||
App Router
|
App Router
|
||||||
</Link>
|
</Link>
|
||||||
{" made explicitly for Payload's "}
|
{' made explicitly for the '}
|
||||||
<Link href="https://github.com/payloadcms/payload/tree/master/examples/auth/cms">
|
<Link href="https://github.com/payloadcms/payload/tree/master/examples/auth">
|
||||||
Auth Example
|
Payload Auth Example
|
||||||
</Link>
|
</Link>
|
||||||
{". This example demonstrates how to implement Payload's "}
|
{". This example demonstrates how to implement Payload's "}
|
||||||
<Link href="https://payloadcms.com/docs/authentication/overview">Authentication</Link>
|
<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>
|
||||||
<p>
|
<p>
|
||||||
{'Visit the '}
|
{'Visit the '}
|
||||||
<Link href="/login">Login</Link>
|
<Link href="/login">login page</Link>
|
||||||
{' page to start the authentication flow. Once logged in, you will be redirected to the '}
|
{' to start the authentication flow. Once logged in, you will be redirected to the '}
|
||||||
<Link href="/account">Account</Link>
|
<Link href="/account">account page</Link>
|
||||||
{` page which is restricted to users only. To toggle APIs, simply toggle the "api" prop between "rest" and "gql" in "_app.tsx".`}
|
{` 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>
|
</p>
|
||||||
</Gutter>
|
</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 { Gutter } from '../_components/Gutter'
|
||||||
import { Input } from '../_components/Input'
|
import { RecoverPasswordForm } from './RecoverPasswordForm'
|
||||||
import classes from './index.module.css'
|
|
||||||
|
|
||||||
type FormData = {
|
import classes from './index.module.scss'
|
||||||
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],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
export default async function RecoverPassword() {
|
||||||
return (
|
return (
|
||||||
<Gutter>
|
<Gutter className={classes.recoverPassword}>
|
||||||
{!success && (
|
<RecoverPasswordForm />
|
||||||
<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>
|
</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 { Gutter } from '../_components/Gutter'
|
||||||
import { Input } from '../_components/Input'
|
import { ResetPasswordForm } from './ResetPasswordForm'
|
||||||
import classes from './index.module.css'
|
|
||||||
|
|
||||||
type FormData = {
|
import classes from './index.module.scss'
|
||||||
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])
|
|
||||||
|
|
||||||
|
export default async function ResetPassword() {
|
||||||
return (
|
return (
|
||||||
<Gutter>
|
<Gutter className={classes.resetPassword}>
|
||||||
<h1>Reset Password</h1>
|
<h1>Reset Password</h1>
|
||||||
<p>Please enter a new password below.</p>
|
<p>Please enter a new password below.</p>
|
||||||
{error && <div className={classes.error}>{error}</div>}
|
<ResetPasswordForm />
|
||||||
<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>
|
|
||||||
</Gutter>
|
</Gutter>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ResetPassword
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Payload Auth Example Front-End
|
# 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).
|
> 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
|
### 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
|
### 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
|
4. `yarn dev` or `npm run dev` to start the server
|
||||||
5. `open http://localhost:3001` to see the result
|
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
|
## 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.
|
- [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.
|
- [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.
|
- [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
|
## 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).
|
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.
|
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 {
|
.gutter {
|
||||||
max-width: var(--max-width);
|
max-width: 1920px;
|
||||||
width: 100%;
|
margin-left: auto;
|
||||||
margin: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gutterLeft {
|
.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 {
|
.header {
|
||||||
padding: var(--base) 0;
|
padding: var(--base) 0;
|
||||||
z-index: var(--header-z-index);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: calc(var(--base) / 2) var(--base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav {
|
.logo {
|
||||||
a {
|
width: 150px;
|
||||||
text-decoration: none;
|
}
|
||||||
margin-left: var(--base);
|
|
||||||
|
: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 Image from 'next/image'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
import { useAuth } from '../Auth'
|
|
||||||
import { Gutter } from '../Gutter'
|
import { Gutter } from '../Gutter'
|
||||||
|
import { HeaderNav } from './Nav'
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
import classes from './index.module.scss'
|
||||||
|
|
||||||
export const Header: React.FC = () => {
|
export const Header: React.FC = () => {
|
||||||
const { user } = useAuth()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={classes.header}>
|
<header className={classes.header}>
|
||||||
<Gutter className={classes.wrap}>
|
<Gutter className={classes.wrap}>
|
||||||
@@ -27,20 +25,7 @@ export const Header: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</picture>
|
</picture>
|
||||||
</Link>
|
</Link>
|
||||||
<nav className={classes.nav}>
|
<HeaderNav />
|
||||||
{!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>
|
|
||||||
</Gutter>
|
</Gutter>
|
||||||
</header>
|
</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 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 = {
|
type Props = {
|
||||||
name: string
|
name: string
|
||||||
@@ -9,7 +9,8 @@ type Props = {
|
|||||||
register: UseFormRegister<FieldValues & any>
|
register: UseFormRegister<FieldValues & any>
|
||||||
required?: boolean
|
required?: boolean
|
||||||
error: any
|
error: any
|
||||||
type?: 'text' | 'number' | 'password'
|
type?: 'text' | 'number' | 'password' | 'email'
|
||||||
|
validate?: (value: string) => boolean | string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Input: React.FC<Props> = ({
|
export const Input: React.FC<Props> = ({
|
||||||
@@ -19,14 +20,36 @@ export const Input: React.FC<Props> = ({
|
|||||||
register,
|
register,
|
||||||
error,
|
error,
|
||||||
type = 'text',
|
type = 'text',
|
||||||
|
validate,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={classes.input}>
|
<div className={classes.inputWrap}>
|
||||||
<label htmlFor="name" className={classes.label}>
|
<label htmlFor="name" className={classes.label}>
|
||||||
{label}
|
{`${label} ${required ? '*' : ''}`}
|
||||||
</label>
|
</label>
|
||||||
<input {...{ type }} {...register(name, { required })} />
|
<input
|
||||||
{error && <div className={classes.error}>This field is required</div>}
|
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>
|
</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 type { AppProps } from 'next/app'
|
||||||
|
|
||||||
import { AuthProvider } from '../components/Auth'
|
|
||||||
import { Header } from '../components/Header'
|
import { Header } from '../components/Header'
|
||||||
|
import { AuthProvider } from '../providers/Auth'
|
||||||
|
|
||||||
import './app.scss'
|
import '../css/app.scss'
|
||||||
|
|
||||||
import classes from './index.module.scss'
|
|
||||||
|
|
||||||
export default function MyApp({ Component, pageProps }: AppProps) {
|
export default function MyApp({ Component, pageProps }: AppProps) {
|
||||||
return (
|
return (
|
||||||
// The `AuthProvider` can be used with either REST or GraphQL APIs
|
<AuthProvider
|
||||||
// Just change the `api` prop to "graphql" or "rest", that's it!
|
// To toggle between the REST and GraphQL APIs,
|
||||||
<AuthProvider api="rest">
|
// change the `api` prop to either `rest` or `gql`
|
||||||
|
api="rest" // change this to `gql` to use the GraphQL API
|
||||||
|
>
|
||||||
<Header />
|
<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
|
Remove these comments when the issue is resolved
|
||||||
See more here: https://github.com/facebook/react/issues/24304
|
See more here: https://github.com/facebook/react/issues/24304
|
||||||
*/}
|
*/}
|
||||||
{/* @ts-expect-error */}
|
{/* @ts-expect-error */}
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</div>
|
|
||||||
</AuthProvider>
|
</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 { useForm } from 'react-hook-form'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
import { useAuth } from '../../components/Auth'
|
import { Button } from '../../components/Button'
|
||||||
import { Gutter } from '../../components/Gutter'
|
import { Gutter } from '../../components/Gutter'
|
||||||
import { Input } from '../../components/Input'
|
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 = {
|
type FormData = {
|
||||||
email: string
|
email: string
|
||||||
firstName: string
|
name: string
|
||||||
lastName: string
|
password: string
|
||||||
|
passwordConfirm: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const Account: React.FC = () => {
|
const Account: React.FC = () => {
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [success, setSuccess] = useState('')
|
const [success, setSuccess] = useState('')
|
||||||
const { user, setUser } = useAuth()
|
const { user, setUser } = useAuth()
|
||||||
|
const [changePassword, setChangePassword] = useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors },
|
formState: { errors, isLoading },
|
||||||
reset,
|
reset,
|
||||||
|
watch,
|
||||||
} = useForm<FormData>()
|
} = useForm<FormData>()
|
||||||
|
|
||||||
|
const password = useRef({})
|
||||||
|
password.current = watch('password', '')
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
async (data: FormData) => {
|
async (data: FormData) => {
|
||||||
if (user) {
|
if (user) {
|
||||||
@@ -42,21 +52,22 @@ const Account: React.FC = () => {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
|
|
||||||
// Update the user in auth state with new values
|
|
||||||
setUser(json.doc)
|
setUser(json.doc)
|
||||||
|
|
||||||
// Set success message for user
|
|
||||||
setSuccess('Successfully updated account.')
|
setSuccess('Successfully updated account.')
|
||||||
|
|
||||||
// Clear any existing errors
|
|
||||||
setError('')
|
setError('')
|
||||||
|
setChangePassword(false)
|
||||||
|
reset({
|
||||||
|
email: json.doc.email,
|
||||||
|
name: json.doc.name,
|
||||||
|
password: '',
|
||||||
|
passwordConfirm: '',
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
setError('There was a problem updating your account.')
|
setError('There was a problem updating your account.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[user, setUser],
|
[user, setUser, reset],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -68,37 +79,87 @@ const Account: React.FC = () => {
|
|||||||
if (user) {
|
if (user) {
|
||||||
reset({
|
reset({
|
||||||
email: user.email,
|
email: user.email,
|
||||||
firstName: user.firstName,
|
password: '',
|
||||||
lastName: user.lastName,
|
passwordConfirm: '',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [user, reset, router])
|
}, [user, router, reset, changePassword])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof router.query.success === 'string') {
|
|
||||||
setSuccess(router.query.success)
|
|
||||||
}
|
|
||||||
}, [router])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Gutter>
|
<Gutter className={classes.account}>
|
||||||
|
<RenderParams className={classes.params} />
|
||||||
<h1>Account</h1>
|
<h1>Account</h1>
|
||||||
{router.query.message && <div className={classes.message}>{router.query.message}</div>}
|
<p>
|
||||||
{error && <div className={classes.error}>{error}</div>}
|
{`This is your account dashboard. Here you can update your account information and more. To manage all users, `}
|
||||||
{success && <div className={classes.success}>{success}</div>}
|
<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}>
|
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
||||||
|
<Message error={error} success={success} className={classes.message} />
|
||||||
|
{!changePassword ? (
|
||||||
|
<Fragment>
|
||||||
|
<p>
|
||||||
|
{'Change your account details below, or '}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={classes.changePassword}
|
||||||
|
onClick={() => setChangePassword(!changePassword)}
|
||||||
|
>
|
||||||
|
click here
|
||||||
|
</button>
|
||||||
|
{' to change your password.'}
|
||||||
|
</p>
|
||||||
<Input
|
<Input
|
||||||
name="email"
|
name="email"
|
||||||
label="Email Address"
|
label="Email Address"
|
||||||
required
|
required
|
||||||
register={register}
|
register={register}
|
||||||
error={errors.email}
|
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>
|
</form>
|
||||||
<Link href="/logout">Log out</Link>
|
<Button href="/logout" appearance="secondary" label="Log out" />
|
||||||
</Gutter>
|
</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,57 +1,96 @@
|
|||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import Link from 'next/link'
|
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 { Gutter } from '../../components/Gutter'
|
||||||
import { Input } from '../../components/Input'
|
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 = {
|
type FormData = {
|
||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
firstName: string
|
passwordConfirm: string
|
||||||
lastName: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateAccount: React.FC = () => {
|
const CreateAccount: React.FC = () => {
|
||||||
const [error, setError] = useState('')
|
const router = useRouter()
|
||||||
const [success, setSuccess] = useState(false)
|
const searchParams = useMemo(() => new URLSearchParams(router.query as any), [router.query])
|
||||||
const { login, create, user } = useAuth()
|
const allParams = searchParams.toString() ? `?${searchParams.toString()}` : ''
|
||||||
|
const { login } = useAuth()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
|
watch,
|
||||||
} = useForm<FormData>()
|
} = useForm<FormData>()
|
||||||
|
|
||||||
|
const password = useRef({})
|
||||||
|
password.current = watch('password', '')
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
async (data: FormData) => {
|
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 {
|
try {
|
||||||
await create(data as Parameters<typeof create>[0])
|
await login(data)
|
||||||
// Automatically log the user in after creating their account
|
clearTimeout(timer)
|
||||||
await login({ email: data.email, password: data.password })
|
if (redirect) router.push(redirect as string)
|
||||||
setSuccess(true)
|
else router.push(`/account?success=${encodeURIComponent('Account created successfully')}`)
|
||||||
} catch (err) {
|
} catch (_) {
|
||||||
setError(err?.message || 'An error occurred while attempting to create your account.')
|
clearTimeout(timer)
|
||||||
|
setError('There was an error with the credentials provided. Please try again.')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[login, create],
|
[login, router, searchParams],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Gutter>
|
<Gutter className={classes.createAccount}>
|
||||||
{!success && (
|
|
||||||
<React.Fragment>
|
|
||||||
<h1>Create Account</h1>
|
<h1>Create Account</h1>
|
||||||
{error && <div className={classes.error}>{error}</div>}
|
<RenderParams />
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
<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
|
<Input
|
||||||
name="email"
|
name="email"
|
||||||
label="Email Address"
|
label="Email Address"
|
||||||
required
|
required
|
||||||
register={register}
|
register={register}
|
||||||
error={errors.email}
|
error={errors.email}
|
||||||
|
type="email"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
name="password"
|
name="password"
|
||||||
@@ -62,27 +101,25 @@ const CreateAccount: React.FC = () => {
|
|||||||
error={errors.password}
|
error={errors.password}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
name="firstName"
|
name="passwordConfirm"
|
||||||
label="First Name"
|
type="password"
|
||||||
|
label="Confirm Password"
|
||||||
|
required
|
||||||
register={register}
|
register={register}
|
||||||
error={errors.firstName}
|
validate={value => value === password.current || 'The passwords do not match'}
|
||||||
|
error={errors.passwordConfirm}
|
||||||
/>
|
/>
|
||||||
<Input name="lastName" label="Last Name" register={register} error={errors.lastName} />
|
<Button
|
||||||
<button type="submit">Create account</button>
|
type="submit"
|
||||||
</form>
|
className={classes.submit}
|
||||||
<p>
|
label={loading ? 'Processing' : 'Create Account'}
|
||||||
|
appearance="primary"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
{'Already have an account? '}
|
{'Already have an account? '}
|
||||||
<Link href="/login">Login</Link>
|
<Link href={`/login${allParams}`}>Login</Link>
|
||||||
</p>
|
</div>
|
||||||
</React.Fragment>
|
</form>
|
||||||
)}
|
|
||||||
{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>
|
</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 Link from 'next/link'
|
||||||
|
|
||||||
import { Gutter } from '../components/Gutter'
|
import { Gutter } from '../components/Gutter'
|
||||||
|
|
||||||
const Home: React.FC = () => {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<Gutter>
|
<Gutter>
|
||||||
<h1>Payload Auth Example</h1>
|
<h1>Payload Auth Example</h1>
|
||||||
@@ -13,40 +12,34 @@ const Home: React.FC = () => {
|
|||||||
Payload
|
Payload
|
||||||
</Link>
|
</Link>
|
||||||
{' + '}
|
{' + '}
|
||||||
<Link href="https://nextjs.org/" target="_blank" rel="noopener noreferrer">
|
<Link href="https://nextjs.org" target="_blank" rel="noopener noreferrer">
|
||||||
Next.js
|
Next.js
|
||||||
</Link>
|
</Link>
|
||||||
{' app using the '}
|
{' app using the '}
|
||||||
<Link href="https://nextjs.org/docs/pages" target="_blank" rel="noopener noreferrer">
|
<Link href="https://nextjs.org/docs/pages" target="_blank" rel="noopener noreferrer">
|
||||||
Pages Router
|
Pages Router
|
||||||
</Link>
|
</Link>
|
||||||
{" made explicitly for Payload's "}
|
{' made explicitly for the '}
|
||||||
<Link
|
<Link href="https://github.com/payloadcms/payload/tree/master/examples/auth">
|
||||||
href="https://github.com/payloadcms/payload/tree/master/examples/auth/cms"
|
Payload Auth Example
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Auth Example
|
|
||||||
</Link>
|
</Link>
|
||||||
{". This example demonstrates how to implement Payload's "}
|
{". This example demonstrates how to implement Payload's "}
|
||||||
<Link
|
<Link href="https://payloadcms.com/docs/authentication/overview">Authentication</Link>
|
||||||
href="https://payloadcms.com/docs/authentication/overview"
|
{
|
||||||
target="_blank"
|
' strategies in both the REST and GraphQL APIs. To toggle between these APIs, see `_app.tsx`.'
|
||||||
rel="noopener noreferrer"
|
}
|
||||||
>
|
|
||||||
Authentication
|
|
||||||
</Link>
|
|
||||||
{' strategies in both the REST and GraphQL APIs.'}
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{'Visit the '}
|
{'Visit the '}
|
||||||
<Link href="/login">Login</Link>
|
<Link href="/login">login page</Link>
|
||||||
{' page to start the authentication flow. Once logged in, you will be redirected to the '}
|
{' to start the authentication flow. Once logged in, you will be redirected to the '}
|
||||||
<Link href="/account">Account</Link>
|
<Link href="/account">account page</Link>
|
||||||
{` page which is restricted to users only. To toggle APIs, simply toggle the "api" prop between "rest" and "gql" in "_app.tsx".`}
|
{` 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>
|
</p>
|
||||||
</Gutter>
|
</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 { useForm } from 'react-hook-form'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
import { useAuth } from '../../components/Auth'
|
import { Button } from '../../components/Button'
|
||||||
import { Gutter } from '../../components/Gutter'
|
import { Gutter } from '../../components/Gutter'
|
||||||
import { Input } from '../../components/Input'
|
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 = {
|
type FormData = {
|
||||||
email: string
|
email: string
|
||||||
@@ -14,52 +18,61 @@ type FormData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Login: React.FC = () => {
|
const Login: React.FC = () => {
|
||||||
const [error, setError] = useState('')
|
|
||||||
const router = useRouter()
|
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 {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors },
|
formState: { errors, isLoading },
|
||||||
} = useForm<FormData>()
|
} = useForm<FormData>({
|
||||||
|
defaultValues: {
|
||||||
|
email: 'demo@payloadcms.com',
|
||||||
|
password: 'demo',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
async (data: FormData) => {
|
async (data: FormData) => {
|
||||||
try {
|
try {
|
||||||
await login(data)
|
await login(data)
|
||||||
router.push('/account')
|
if (redirect?.current) router.push(redirect.current as string)
|
||||||
} catch (err) {
|
else router.push('/account')
|
||||||
setError(err?.message || 'An error occurred while attempting to login.')
|
} catch (_) {
|
||||||
|
setError('There was an error with the credentials provided. Please try again.')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[login, router],
|
[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 (
|
return (
|
||||||
<Gutter>
|
<Gutter className={classes.login}>
|
||||||
|
<RenderParams className={classes.params} />
|
||||||
<h1>Log in</h1>
|
<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}>
|
<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
|
<Input
|
||||||
name="email"
|
name="email"
|
||||||
label="Email Address"
|
label="Email Address"
|
||||||
required
|
required
|
||||||
register={register}
|
register={register}
|
||||||
error={errors.email}
|
error={errors.email}
|
||||||
|
type="email"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
name="password"
|
name="password"
|
||||||
@@ -69,11 +82,19 @@ const Login: React.FC = () => {
|
|||||||
register={register}
|
register={register}
|
||||||
error={errors.password}
|
error={errors.password}
|
||||||
/>
|
/>
|
||||||
<input type="submit" />
|
<Button
|
||||||
</form>
|
type="submit"
|
||||||
<Link href="/create-account">Create an account</Link>
|
disabled={isLoading}
|
||||||
|
className={classes.submit}
|
||||||
|
label={isLoading ? 'Processing' : 'Login'}
|
||||||
|
appearance="primary"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Link href={`/create-account${allParams}`}>Create an account</Link>
|
||||||
<br />
|
<br />
|
||||||
<Link href="/recover-password">Recover your password</Link>
|
<Link href={`/recover-password${allParams}`}>Recover your password</Link>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</Gutter>
|
</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 Link from 'next/link'
|
||||||
|
|
||||||
import { useAuth } from '../../components/Auth'
|
|
||||||
import { Gutter } from '../../components/Gutter'
|
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: React.FC = () => {
|
||||||
const { logout } = useAuth()
|
const { logout } = useAuth()
|
||||||
@@ -15,8 +16,8 @@ const Logout: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
await logout()
|
await logout()
|
||||||
setSuccess('Logged out successfully.')
|
setSuccess('Logged out successfully.')
|
||||||
} catch (err) {
|
} catch (_) {
|
||||||
setError(err?.message || 'An error occurred while attempting to logout.')
|
setError('You are already logged out.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,17 +25,19 @@ const Logout: React.FC = () => {
|
|||||||
}, [logout])
|
}, [logout])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Gutter>
|
<Gutter className={classes.logout}>
|
||||||
{success && <h1>{success}</h1>}
|
{(error || success) && (
|
||||||
{error && <div className={classes.error}>{error}</div>}
|
<div>
|
||||||
|
<h1>{error || success}</h1>
|
||||||
<p>
|
<p>
|
||||||
{'What would you like to do next? '}
|
{'What would you like to do next? '}
|
||||||
<Fragment>
|
<Link href="/">Click here</Link>
|
||||||
{' To log back in, '}
|
{` to go to the home page. To log back in, `}
|
||||||
<Link href={`/login`}>click here</Link>
|
<Link href="login">click here</Link>
|
||||||
{'.'}
|
{'.'}
|
||||||
</Fragment>
|
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Gutter>
|
</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 React, { useCallback, useState } from 'react'
|
||||||
import { useForm } from 'react-hook-form'
|
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 { Gutter } from '../../components/Gutter'
|
||||||
import { Input } from '../../components/Input'
|
import { Input } from '../../components/Input'
|
||||||
import classes from './index.module.css'
|
import { Message } from '../../components/Message'
|
||||||
|
|
||||||
|
import classes from './index.module.scss'
|
||||||
|
|
||||||
type FormData = {
|
type FormData = {
|
||||||
email: string
|
email: string
|
||||||
@@ -13,7 +16,6 @@ type FormData = {
|
|||||||
const RecoverPassword: React.FC = () => {
|
const RecoverPassword: React.FC = () => {
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [success, setSuccess] = useState(false)
|
const [success, setSuccess] = useState(false)
|
||||||
const { forgotPassword } = useAuth()
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -21,42 +23,57 @@ const RecoverPassword: React.FC = () => {
|
|||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<FormData>()
|
} = useForm<FormData>()
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(async (data: FormData) => {
|
||||||
async (data: FormData) => {
|
const response = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/forgot-password`, {
|
||||||
try {
|
method: 'POST',
|
||||||
const user = await forgotPassword(data as Parameters<typeof forgotPassword>[0])
|
body: JSON.stringify(data),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
if (user) {
|
if (response.ok) {
|
||||||
setSuccess(true)
|
setSuccess(true)
|
||||||
setError('')
|
setError('')
|
||||||
}
|
} else {
|
||||||
} catch (err) {
|
setError(
|
||||||
setError(err?.message || 'An error occurred while attempting to recover password.')
|
'There was a problem while attempting to send you a password reset email. Please try again.',
|
||||||
}
|
|
||||||
},
|
|
||||||
[forgotPassword],
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Gutter>
|
<Gutter className={classes.recoverPassword}>
|
||||||
{!success && (
|
{!success && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<h1>Recover Password</h1>
|
<h1>Recover Password</h1>
|
||||||
|
<div className={classes.formWrapper}>
|
||||||
<p>
|
<p>
|
||||||
Please enter your email below. You will receive an email message with instructions on
|
{`Please enter your email below. You will receive an email message with instructions on
|
||||||
how to reset your password.
|
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>
|
</p>
|
||||||
{error && <div className={classes.error}>{error}</div>}
|
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<Message error={error} className={classes.message} />
|
||||||
<Input
|
<Input
|
||||||
name="email"
|
name="email"
|
||||||
label="Email Address"
|
label="Email Address"
|
||||||
required
|
required
|
||||||
register={register}
|
register={register}
|
||||||
error={errors.email}
|
error={errors.email}
|
||||||
|
type="email"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className={classes.submit}
|
||||||
|
label="Recover Password"
|
||||||
|
appearance="primary"
|
||||||
/>
|
/>
|
||||||
<button type="submit">Submit</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
{success && (
|
{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