From 2a932ea28e50e4aa10b598e7cc194ff6d54c7cea Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Thu, 20 Jul 2023 14:06:26 -0400 Subject: [PATCH] chore: updates auth example (#3026) --- examples/auth/cms/README.md | 7 +- examples/auth/next-app/README.md | 10 +- .../app/_components/Button/index.module.scss | 40 +++ .../next-app/app/_components/Button/index.tsx | 75 ++++++ .../app/_components/Gutter/index.module.scss | 6 +- .../_components/Header/Nav/index.module.scss | 20 ++ .../app/_components/Header/Nav/index.tsx | 38 +++ .../app/_components/Header/index.client.tsx | 31 --- .../app/_components/Header/index.module.scss | 24 +- .../next-app/app/_components/Header/index.tsx | 4 +- .../app/_components/Input/index.module.css | 23 -- .../app/_components/Input/index.module.scss | 56 ++++ .../next-app/app/_components/Input/index.tsx | 37 ++- .../app/_components/Message/index.module.scss | 46 ++++ .../app/_components/Message/index.tsx | 33 +++ .../app/_components/RenderParams/index.tsx | 29 +++ examples/auth/next-app/app/_css/app.scss | 117 +++++++++ examples/auth/next-app/app/_css/colors.scss | 83 ++++++ examples/auth/next-app/app/_css/common.scss | 1 + examples/auth/next-app/app/_css/queries.scss | 28 ++ examples/auth/next-app/app/_css/theme.scss | 241 ++++++++++++++++++ examples/auth/next-app/app/_css/type.scss | 110 ++++++++ .../{_components => _providers}/Auth/gql.ts | 0 .../Auth/index.tsx | 0 .../{_components => _providers}/Auth/rest.ts | 0 .../{_components => _providers}/Auth/types.ts | 0 .../auth/next-app/app/_utilities/getMeUser.ts | 41 +++ .../app/account/AccountForm/index.module.scss | 24 ++ .../app/account/AccountForm/index.tsx | 152 +++++++++++ .../next-app/app/account/index.module.css | 17 -- .../next-app/app/account/index.module.scss | 7 + examples/auth/next-app/app/account/page.tsx | 123 ++------- examples/auth/next-app/app/app.scss | 107 -------- .../CreateAccountForm/index.module.scss | 22 ++ .../CreateAccountForm/index.tsx | 121 +++++++++ .../app/create-account/index.module.css | 8 - .../app/create-account/index.module.scss | 5 + .../auth/next-app/app/create-account/page.tsx | 98 ++----- examples/auth/next-app/app/index.module.scss | 3 - examples/auth/next-app/app/layout.tsx | 14 +- .../app/login/LoginForm/index.module.scss | 22 ++ .../next-app/app/login/LoginForm/index.tsx | 96 +++++++ .../auth/next-app/app/login/index.module.css | 8 - .../auth/next-app/app/login/index.module.scss | 9 + examples/auth/next-app/app/login/page.tsx | 87 +------ .../next-app/app/logout/LogoutPage/index.tsx | 42 +++ .../auth/next-app/app/logout/index.module.css | 4 - .../next-app/app/logout/index.module.scss | 3 + examples/auth/next-app/app/logout/page.tsx | 42 +-- examples/auth/next-app/app/page.tsx | 22 +- .../RecoverPasswordForm/index.module.scss | 23 ++ .../RecoverPasswordForm/index.tsx | 88 +++++++ .../app/recover-password/index.module.css | 4 - .../app/recover-password/index.module.scss | 5 + .../next-app/app/recover-password/page.tsx | 72 +----- .../ResetPasswordForm/index.module.scss | 13 + .../ResetPasswordForm/index.tsx | 84 ++++++ .../app/reset-password/index.module.css | 4 - .../app/reset-password/index.module.scss | 3 + .../auth/next-app/app/reset-password/page.tsx | 71 +----- examples/auth/next-pages/README.md | 14 +- .../src/components/Button/index.module.scss | 40 +++ .../src/components/Button/index.tsx | 73 ++++++ .../src/components/Gutter/index.module.scss | 6 +- .../components/Header/Nav/index.module.scss | 20 ++ .../src/components/Header/Nav/index.tsx | 36 +++ .../src/components/Header/index.module.scss | 16 +- .../src/components/Header/index.tsx | 21 +- .../src/components/Input/index.module.css | 23 -- .../src/components/Input/index.module.scss | 56 ++++ .../next-pages/src/components/Input/index.tsx | 37 ++- .../src/components/Message/index.module.scss | 46 ++++ .../src/components/Message/index.tsx | 33 +++ .../src/components/RenderParams/index.tsx | 25 ++ examples/auth/next-pages/src/css/app.scss | 117 +++++++++ examples/auth/next-pages/src/css/colors.scss | 83 ++++++ examples/auth/next-pages/src/css/common.scss | 1 + examples/auth/next-pages/src/css/queries.scss | 28 ++ examples/auth/next-pages/src/css/theme.scss | 241 ++++++++++++++++++ examples/auth/next-pages/src/css/type.scss | 110 ++++++++ examples/auth/next-pages/src/pages/_app.tsx | 22 +- .../src/pages/account/index.module.css | 17 -- .../src/pages/account/index.module.scss | 32 +++ .../next-pages/src/pages/account/index.tsx | 133 +++++++--- examples/auth/next-pages/src/pages/app.scss | 107 -------- .../src/pages/create-account/index.module.css | 8 - .../pages/create-account/index.module.scss | 26 ++ .../src/pages/create-account/index.tsx | 153 ++++++----- .../next-pages/src/pages/index.module.scss | 3 - examples/auth/next-pages/src/pages/index.tsx | 41 ++- .../src/pages/login/index.module.css | 8 - .../src/pages/login/index.module.scss | 30 +++ .../auth/next-pages/src/pages/login/index.tsx | 79 +++--- .../src/pages/logout/index.module.css | 4 - .../src/pages/logout/index.module.scss | 3 + .../next-pages/src/pages/logout/index.tsx | 35 +-- .../pages/recover-password/index.module.css | 4 - .../pages/recover-password/index.module.scss | 27 ++ .../src/pages/recover-password/index.tsx | 83 +++--- .../src/pages/reset-password/index.module.css | 4 - .../pages/reset-password/index.module.scss | 17 ++ .../src/pages/reset-password/index.tsx | 62 +++-- .../src/{components => providers}/Auth/gql.ts | 0 .../{components => providers}/Auth/index.tsx | 0 .../{components => providers}/Auth/rest.ts | 0 .../{components => providers}/Auth/types.ts | 0 106 files changed, 3298 insertions(+), 1129 deletions(-) create mode 100644 examples/auth/next-app/app/_components/Button/index.module.scss create mode 100644 examples/auth/next-app/app/_components/Button/index.tsx create mode 100644 examples/auth/next-app/app/_components/Header/Nav/index.module.scss create mode 100644 examples/auth/next-app/app/_components/Header/Nav/index.tsx delete mode 100644 examples/auth/next-app/app/_components/Header/index.client.tsx delete mode 100644 examples/auth/next-app/app/_components/Input/index.module.css create mode 100644 examples/auth/next-app/app/_components/Input/index.module.scss create mode 100644 examples/auth/next-app/app/_components/Message/index.module.scss create mode 100644 examples/auth/next-app/app/_components/Message/index.tsx create mode 100644 examples/auth/next-app/app/_components/RenderParams/index.tsx create mode 100644 examples/auth/next-app/app/_css/app.scss create mode 100644 examples/auth/next-app/app/_css/colors.scss create mode 100644 examples/auth/next-app/app/_css/common.scss create mode 100644 examples/auth/next-app/app/_css/queries.scss create mode 100644 examples/auth/next-app/app/_css/theme.scss create mode 100644 examples/auth/next-app/app/_css/type.scss rename examples/auth/next-app/app/{_components => _providers}/Auth/gql.ts (100%) rename examples/auth/next-app/app/{_components => _providers}/Auth/index.tsx (100%) rename examples/auth/next-app/app/{_components => _providers}/Auth/rest.ts (100%) rename examples/auth/next-app/app/{_components => _providers}/Auth/types.ts (100%) create mode 100644 examples/auth/next-app/app/_utilities/getMeUser.ts create mode 100644 examples/auth/next-app/app/account/AccountForm/index.module.scss create mode 100644 examples/auth/next-app/app/account/AccountForm/index.tsx delete mode 100644 examples/auth/next-app/app/account/index.module.css create mode 100644 examples/auth/next-app/app/account/index.module.scss delete mode 100644 examples/auth/next-app/app/app.scss create mode 100644 examples/auth/next-app/app/create-account/CreateAccountForm/index.module.scss create mode 100644 examples/auth/next-app/app/create-account/CreateAccountForm/index.tsx delete mode 100644 examples/auth/next-app/app/create-account/index.module.css create mode 100644 examples/auth/next-app/app/create-account/index.module.scss delete mode 100644 examples/auth/next-app/app/index.module.scss create mode 100644 examples/auth/next-app/app/login/LoginForm/index.module.scss create mode 100644 examples/auth/next-app/app/login/LoginForm/index.tsx delete mode 100644 examples/auth/next-app/app/login/index.module.css create mode 100644 examples/auth/next-app/app/login/index.module.scss create mode 100644 examples/auth/next-app/app/logout/LogoutPage/index.tsx delete mode 100644 examples/auth/next-app/app/logout/index.module.css create mode 100644 examples/auth/next-app/app/logout/index.module.scss create mode 100644 examples/auth/next-app/app/recover-password/RecoverPasswordForm/index.module.scss create mode 100644 examples/auth/next-app/app/recover-password/RecoverPasswordForm/index.tsx delete mode 100644 examples/auth/next-app/app/recover-password/index.module.css create mode 100644 examples/auth/next-app/app/recover-password/index.module.scss create mode 100644 examples/auth/next-app/app/reset-password/ResetPasswordForm/index.module.scss create mode 100644 examples/auth/next-app/app/reset-password/ResetPasswordForm/index.tsx delete mode 100644 examples/auth/next-app/app/reset-password/index.module.css create mode 100644 examples/auth/next-app/app/reset-password/index.module.scss create mode 100644 examples/auth/next-pages/src/components/Button/index.module.scss create mode 100644 examples/auth/next-pages/src/components/Button/index.tsx create mode 100644 examples/auth/next-pages/src/components/Header/Nav/index.module.scss create mode 100644 examples/auth/next-pages/src/components/Header/Nav/index.tsx delete mode 100644 examples/auth/next-pages/src/components/Input/index.module.css create mode 100644 examples/auth/next-pages/src/components/Input/index.module.scss create mode 100644 examples/auth/next-pages/src/components/Message/index.module.scss create mode 100644 examples/auth/next-pages/src/components/Message/index.tsx create mode 100644 examples/auth/next-pages/src/components/RenderParams/index.tsx create mode 100644 examples/auth/next-pages/src/css/app.scss create mode 100644 examples/auth/next-pages/src/css/colors.scss create mode 100644 examples/auth/next-pages/src/css/common.scss create mode 100644 examples/auth/next-pages/src/css/queries.scss create mode 100644 examples/auth/next-pages/src/css/theme.scss create mode 100644 examples/auth/next-pages/src/css/type.scss delete mode 100644 examples/auth/next-pages/src/pages/account/index.module.css create mode 100644 examples/auth/next-pages/src/pages/account/index.module.scss delete mode 100644 examples/auth/next-pages/src/pages/app.scss delete mode 100644 examples/auth/next-pages/src/pages/create-account/index.module.css create mode 100644 examples/auth/next-pages/src/pages/create-account/index.module.scss delete mode 100644 examples/auth/next-pages/src/pages/index.module.scss delete mode 100644 examples/auth/next-pages/src/pages/login/index.module.css create mode 100644 examples/auth/next-pages/src/pages/login/index.module.scss delete mode 100644 examples/auth/next-pages/src/pages/logout/index.module.css create mode 100644 examples/auth/next-pages/src/pages/logout/index.module.scss delete mode 100644 examples/auth/next-pages/src/pages/recover-password/index.module.css create mode 100644 examples/auth/next-pages/src/pages/recover-password/index.module.scss delete mode 100644 examples/auth/next-pages/src/pages/reset-password/index.module.css create mode 100644 examples/auth/next-pages/src/pages/reset-password/index.module.scss rename examples/auth/next-pages/src/{components => providers}/Auth/gql.ts (100%) rename examples/auth/next-pages/src/{components => providers}/Auth/index.tsx (100%) rename examples/auth/next-pages/src/{components => providers}/Auth/rest.ts (100%) rename examples/auth/next-pages/src/{components => providers}/Auth/types.ts (100%) diff --git a/examples/auth/cms/README.md b/examples/auth/cms/README.md index 4ceee99db..54f62a4e0 100644 --- a/examples/auth/cms/README.md +++ b/examples/auth/cms/README.md @@ -1,8 +1,11 @@ # Payload Auth Example -This example demonstrates how to implement [Payload Authentication](https://payloadcms.com/docs/authentication/overview). +This example demonstrates how to implement [Payload Authentication](https://payloadcms.com/docs/authentication/overview). Follow the [Quick Start](#quick-start) to get up and running quickly. There are various fully working front-ends made explicitly for this example, including: -There is a fully working Next.js app made explicitly for this example which can be found [here](../next-app). Follow the instructions there to get started. If you are setting up authentication for another front-end, please consider contributing to this repo with your own example! +- [Next.js App Router](../next-app) +- [Next.js Pages Router](../next-pages) + +Follow the instructions in each respective README to get started. If you are setting up authentication for another front-end, please consider contributing to this repo with your own example! ## Quick Start diff --git a/examples/auth/next-app/README.md b/examples/auth/next-app/README.md index b0140e6ea..b6623743d 100644 --- a/examples/auth/next-app/README.md +++ b/examples/auth/next-app/README.md @@ -1,6 +1,6 @@ # Payload Auth Example Front-End -This is a [Next.js](https://nextjs.org) app using the [App Router](https://nextjs.org/docs/app). It was made explicitly for Payload's [Auth Example](https://github.com/payloadcms/payload/tree/master/examples/auth/cms). +This is a [Payload](https://payloadcms.com) + [Next.js](https://nextjs.org) app using the [App Router](https://nextjs.org/docs/app) made explicitly for the [Payload Auth Example](https://github.com/payloadcms/payload/tree/master/examples/auth). It demonstrates how to authenticate your Next.js app using [Payload Authentication](https://payloadcms.com/docs/authentication/overview). > This example uses the App Router, the latest API of Next.js. If your app is using the legacy [Pages Router](https://nextjs.org/docs/pages), check out the official [Pages Router Example](https://github.com/payloadcms/payload/tree/master/examples/auth/next-pages). @@ -8,7 +8,7 @@ This is a [Next.js](https://nextjs.org) app using the [App Router](https://nextj ### Payload -First you'll need a running [Payload](https://github.com/payloadcms/payload) app. If you have not done so already, open up the `cms` folder and follow the setup instructions. Take note of your `serverURL`, you'll need this in the next step. +First you'll need a running Payload app. If you have not done so already, clone down the [`cms`](../cms) folder and follow the setup instructions there to get it up and running. This will provide all the necessary APIs that your Next.js app will be using for authentication. ### Next.js @@ -18,7 +18,7 @@ First you'll need a running [Payload](https://github.com/payloadcms/payload) app 4. `yarn dev` or `npm run dev` to start the server 5. `open http://localhost:3001` to see the result -Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Auth Example](https://github.com/payloadcms/payload/tree/master/examples/auth/cms) for full details. +Once running, a user is automatically seeded in your local environment with some basic instructions. See the [Payload Auth Example](https://github.com/payloadcms/payload/tree/master/examples/auth) for full details. ## Learn More @@ -35,3 +35,7 @@ You can check out [the Payload GitHub repository](https://github.com/payloadcms/ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new) from the creators of Next.js. You could also combine this app into a [single Express server](https://github.com/payloadcms/payload/tree/master/examples/custom-server) and deploy in to [Payload Cloud](https://payloadcms.com/new/import). Check out our [Payload deployment documentation](https://payloadcms.com/docs/production/deployment) or the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. + +## Questions + +If you have any issues or questions, reach out to us on [Discord](https://discord.com/invite/payload) or start a [GitHub discussion](https://github.com/payloadcms/payload/discussions). diff --git a/examples/auth/next-app/app/_components/Button/index.module.scss b/examples/auth/next-app/app/_components/Button/index.module.scss new file mode 100644 index 000000000..aa8b5ac78 --- /dev/null +++ b/examples/auth/next-app/app/_components/Button/index.module.scss @@ -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); +} diff --git a/examples/auth/next-app/app/_components/Button/index.tsx b/examples/auth/next-app/app/_components/Button/index.tsx new file mode 100644 index 000000000..eb4e2737b --- /dev/null +++ b/examples/auth/next-app/app/_components/Button/index.tsx @@ -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 = ({ + 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 = ( +
+ {label} +
+ ) + + if (onClick || type === 'submit') el = 'button' + + if (el === 'link') { + return ( + + {content} + + ) + } + + const Element: ElementType = el + + return ( + + {content} + + ) +} diff --git a/examples/auth/next-app/app/_components/Gutter/index.module.scss b/examples/auth/next-app/app/_components/Gutter/index.module.scss index 09f9f61be..be9e377a7 100644 --- a/examples/auth/next-app/app/_components/Gutter/index.module.scss +++ b/examples/auth/next-app/app/_components/Gutter/index.module.scss @@ -1,7 +1,7 @@ .gutter { - max-width: var(--max-width); - width: 100%; - margin: auto; + max-width: 1920px; + margin-left: auto; + margin-right: auto; } .gutterLeft { diff --git a/examples/auth/next-app/app/_components/Header/Nav/index.module.scss b/examples/auth/next-app/app/_components/Header/Nav/index.module.scss new file mode 100644 index 000000000..6aa7cb85c --- /dev/null +++ b/examples/auth/next-app/app/_components/Header/Nav/index.module.scss @@ -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; +} diff --git a/examples/auth/next-app/app/_components/Header/Nav/index.tsx b/examples/auth/next-app/app/_components/Header/Nav/index.tsx new file mode 100644 index 000000000..0015b0ec8 --- /dev/null +++ b/examples/auth/next-app/app/_components/Header/Nav/index.tsx @@ -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 ( + + ) +} diff --git a/examples/auth/next-app/app/_components/Header/index.client.tsx b/examples/auth/next-app/app/_components/Header/index.client.tsx deleted file mode 100644 index 81683e616..000000000 --- a/examples/auth/next-app/app/_components/Header/index.client.tsx +++ /dev/null @@ -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 ( - - ) -} - -export default HeaderClient diff --git a/examples/auth/next-app/app/_components/Header/index.module.scss b/examples/auth/next-app/app/_components/Header/index.module.scss index e71cfd33f..9478b92ec 100644 --- a/examples/auth/next-app/app/_components/Header/index.module.scss +++ b/examples/auth/next-app/app/_components/Header/index.module.scss @@ -1,3 +1,5 @@ +@use '../../_css/queries.scss' as *; + .header { padding: var(--base) 0; } @@ -5,28 +7,16 @@ .wrap { display: flex; justify-content: space-between; - gap: calc(var(--base) / 2); flex-wrap: wrap; + gap: calc(var(--base) / 2) var(--base); } .logo { - flex-shrink: 0; + width: 150px; } -.nav { - display: flex; - align-items: center; - gap: var(--base); - white-space: nowrap; - overflow: hidden; - flex-wrap: wrap; - - a { - display: block; - text-decoration: none; - } - - @media (max-width: 1000px) { - gap: 0 calc(var(--base) / 2); +:global([data-theme="light"]) { + .logo { + filter: invert(1); } } diff --git a/examples/auth/next-app/app/_components/Header/index.tsx b/examples/auth/next-app/app/_components/Header/index.tsx index 7ac6f746c..ba6015f84 100644 --- a/examples/auth/next-app/app/_components/Header/index.tsx +++ b/examples/auth/next-app/app/_components/Header/index.tsx @@ -3,7 +3,7 @@ import Image from 'next/image' import Link from 'next/link' import { Gutter } from '../Gutter' -import { HeaderClient } from './index.client' +import { HeaderNav } from './Nav' import classes from './index.module.scss' @@ -25,7 +25,7 @@ export function Header() { /> - + ) diff --git a/examples/auth/next-app/app/_components/Input/index.module.css b/examples/auth/next-app/app/_components/Input/index.module.css deleted file mode 100644 index 655128696..000000000 --- a/examples/auth/next-app/app/_components/Input/index.module.css +++ /dev/null @@ -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; -} diff --git a/examples/auth/next-app/app/_components/Input/index.module.scss b/examples/auth/next-app/app/_components/Input/index.module.scss new file mode 100644 index 000000000..e2dfab23b --- /dev/null +++ b/examples/auth/next-app/app/_components/Input/index.module.scss @@ -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; +} diff --git a/examples/auth/next-app/app/_components/Input/index.tsx b/examples/auth/next-app/app/_components/Input/index.tsx index 8d4aaf5b4..771626cf0 100644 --- a/examples/auth/next-app/app/_components/Input/index.tsx +++ b/examples/auth/next-app/app/_components/Input/index.tsx @@ -1,7 +1,7 @@ import React from 'react' -import { FieldValues, UseFormRegister } from 'react-hook-form' +import { FieldValues, UseFormRegister, Validate } from 'react-hook-form' -import classes from './index.module.css' +import classes from './index.module.scss' type Props = { name: string @@ -9,7 +9,8 @@ type Props = { register: UseFormRegister required?: boolean error: any - type?: 'text' | 'number' | 'password' + type?: 'text' | 'number' | 'password' | 'email' + validate?: (value: string) => boolean | string } export const Input: React.FC = ({ @@ -19,14 +20,36 @@ export const Input: React.FC = ({ register, error, type = 'text', + validate, }) => { return ( -
+
- - {error &&
This field is required
} + + {error && ( +
+ {!error?.message && error?.type === 'required' + ? 'This field is required' + : error?.message} +
+ )}
) } diff --git a/examples/auth/next-app/app/_components/Message/index.module.scss b/examples/auth/next-app/app/_components/Message/index.module.scss new file mode 100644 index 000000000..21273d940 --- /dev/null +++ b/examples/auth/next-app/app/_components/Message/index.module.scss @@ -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); + } +} diff --git a/examples/auth/next-app/app/_components/Message/index.tsx b/examples/auth/next-app/app/_components/Message/index.tsx new file mode 100644 index 000000000..3cd806086 --- /dev/null +++ b/examples/auth/next-app/app/_components/Message/index.tsx @@ -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 ( +
+ {messageToRender} +
+ ) + } + return null +} diff --git a/examples/auth/next-app/app/_components/RenderParams/index.tsx b/examples/auth/next-app/app/_components/RenderParams/index.tsx new file mode 100644 index 000000000..b758a3fa8 --- /dev/null +++ b/examples/auth/next-app/app/_components/RenderParams/index.tsx @@ -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 ( +
+ {paramValues.map(paramValue => ( + + ))} +
+ ) + } + + return null +} diff --git a/examples/auth/next-app/app/_css/app.scss b/examples/auth/next-app/app/_css/app.scss new file mode 100644 index 000000000..fcf6af756 --- /dev/null +++ b/examples/auth/next-app/app/_css/app.scss @@ -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; +} diff --git a/examples/auth/next-app/app/_css/colors.scss b/examples/auth/next-app/app/_css/colors.scss new file mode 100644 index 000000000..68bcbc2d5 --- /dev/null +++ b/examples/auth/next-app/app/_css/colors.scss @@ -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); +} diff --git a/examples/auth/next-app/app/_css/common.scss b/examples/auth/next-app/app/_css/common.scss new file mode 100644 index 000000000..bebb9f3aa --- /dev/null +++ b/examples/auth/next-app/app/_css/common.scss @@ -0,0 +1 @@ +@forward './queries.scss'; diff --git a/examples/auth/next-app/app/_css/queries.scss b/examples/auth/next-app/app/_css/queries.scss new file mode 100644 index 000000000..8f84ac709 --- /dev/null +++ b/examples/auth/next-app/app/_css/queries.scss @@ -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; + } +} diff --git a/examples/auth/next-app/app/_css/theme.scss b/examples/auth/next-app/app/_css/theme.scss new file mode 100644 index 000000000..0c93d334f --- /dev/null +++ b/examples/auth/next-app/app/_css/theme.scss @@ -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); + } + } + } +} diff --git a/examples/auth/next-app/app/_css/type.scss b/examples/auth/next-app/app/_css/type.scss new file mode 100644 index 000000000..f8d1d0716 --- /dev/null +++ b/examples/auth/next-app/app/_css/type.scss @@ -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; + } +} diff --git a/examples/auth/next-app/app/_components/Auth/gql.ts b/examples/auth/next-app/app/_providers/Auth/gql.ts similarity index 100% rename from examples/auth/next-app/app/_components/Auth/gql.ts rename to examples/auth/next-app/app/_providers/Auth/gql.ts diff --git a/examples/auth/next-app/app/_components/Auth/index.tsx b/examples/auth/next-app/app/_providers/Auth/index.tsx similarity index 100% rename from examples/auth/next-app/app/_components/Auth/index.tsx rename to examples/auth/next-app/app/_providers/Auth/index.tsx diff --git a/examples/auth/next-app/app/_components/Auth/rest.ts b/examples/auth/next-app/app/_providers/Auth/rest.ts similarity index 100% rename from examples/auth/next-app/app/_components/Auth/rest.ts rename to examples/auth/next-app/app/_providers/Auth/rest.ts diff --git a/examples/auth/next-app/app/_components/Auth/types.ts b/examples/auth/next-app/app/_providers/Auth/types.ts similarity index 100% rename from examples/auth/next-app/app/_components/Auth/types.ts rename to examples/auth/next-app/app/_providers/Auth/types.ts diff --git a/examples/auth/next-app/app/_utilities/getMeUser.ts b/examples/auth/next-app/app/_utilities/getMeUser.ts new file mode 100644 index 000000000..8d361bb42 --- /dev/null +++ b/examples/auth/next-app/app/_utilities/getMeUser.ts @@ -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, + } +} diff --git a/examples/auth/next-app/app/account/AccountForm/index.module.scss b/examples/auth/next-app/app/account/AccountForm/index.module.scss new file mode 100644 index 000000000..568d504e0 --- /dev/null +++ b/examples/auth/next-app/app/account/AccountForm/index.module.scss @@ -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); +} diff --git a/examples/auth/next-app/app/account/AccountForm/index.tsx b/examples/auth/next-app/app/account/AccountForm/index.tsx new file mode 100644 index 000000000..5fe2f8d35 --- /dev/null +++ b/examples/auth/next-app/app/account/AccountForm/index.tsx @@ -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() + + 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 ( +
+ + {!changePassword ? ( + +

+ {'To change your password, '} + + . +

+ +
+ ) : ( + +

+ {'Change your password below, or '} + + . +

+ + value === password.current || 'The passwords do not match'} + error={errors.passwordConfirm} + /> +
+ )} + - - Log out +

+ {`This is your account dashboard. Here you can update your account information and more. To manage all users, `} + + login to the admin dashboard + + {'.'} +

+ + - -

- {'Already have an account? '} - Login -

- - )} - {success && ( - -

Account created successfully

-

You are now logged in.

- Go to your account -
- )} + +

Create Account

+ +
) } - -export default CreateAccount diff --git a/examples/auth/next-app/app/index.module.scss b/examples/auth/next-app/app/index.module.scss deleted file mode 100644 index 3ae99a675..000000000 --- a/examples/auth/next-app/app/index.module.scss +++ /dev/null @@ -1,3 +0,0 @@ -.page { - margin-top: calc(var(--base) * 2); -} diff --git a/examples/auth/next-app/app/layout.tsx b/examples/auth/next-app/app/layout.tsx index 6080261d3..1511091ec 100644 --- a/examples/auth/next-app/app/layout.tsx +++ b/examples/auth/next-app/app/layout.tsx @@ -1,9 +1,7 @@ -import { AuthProvider } from './_components/Auth' import { Header } from './_components/Header' +import { AuthProvider } from './_providers/Auth' -import './app.scss' - -import classes from './index.module.scss' +import './_css/app.scss' export const metadata = { title: 'Payload Auth + Next.js App Router Example', @@ -16,9 +14,13 @@ export default async function RootLayout(props: { children: React.ReactNode }) { return ( - +
-
{children}
+
{children}
diff --git a/examples/auth/next-app/app/login/LoginForm/index.module.scss b/examples/auth/next-app/app/login/LoginForm/index.module.scss new file mode 100644 index 000000000..9c686ec6d --- /dev/null +++ b/examples/auth/next-app/app/login/LoginForm/index.module.scss @@ -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); +} diff --git a/examples/auth/next-app/app/login/LoginForm/index.tsx b/examples/auth/next-app/app/login/LoginForm/index.tsx new file mode 100644 index 000000000..865985ee8 --- /dev/null +++ b/examples/auth/next-app/app/login/LoginForm/index.tsx @@ -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(null) + + const { + register, + handleSubmit, + formState: { errors, isLoading }, + } = useForm({ + 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 ( +
+

+ {'To log in, use the email '} + demo@payloadcms.com + {' with the password '} + demo + {'. To manage your users, '} + + login to the admin dashboard + + . +

+ + + +
+ + )} + {success && ( + +

Request submitted

+

Check your email for a link that will allow you to securely reset your password.

+
+ )} + + ) +} diff --git a/examples/auth/next-app/app/recover-password/index.module.css b/examples/auth/next-app/app/recover-password/index.module.css deleted file mode 100644 index 7d2f0a0af..000000000 --- a/examples/auth/next-app/app/recover-password/index.module.css +++ /dev/null @@ -1,4 +0,0 @@ -.error { - color: red; - margin-bottom: 30px; -} diff --git a/examples/auth/next-app/app/recover-password/index.module.scss b/examples/auth/next-app/app/recover-password/index.module.scss new file mode 100644 index 000000000..2967d35ee --- /dev/null +++ b/examples/auth/next-app/app/recover-password/index.module.scss @@ -0,0 +1,5 @@ +@import "../_css/common"; + +.recoverPassword { + margin-bottom: var(--block-padding); +} diff --git a/examples/auth/next-app/app/recover-password/page.tsx b/examples/auth/next-app/app/recover-password/page.tsx index eb4032e1d..48c9661e3 100644 --- a/examples/auth/next-app/app/recover-password/page.tsx +++ b/examples/auth/next-app/app/recover-password/page.tsx @@ -1,74 +1,14 @@ -'use client' +import React from 'react' -import React, { useCallback, useState } from 'react' -import { useForm } from 'react-hook-form' - -import { useAuth } from '../_components/Auth' import { Gutter } from '../_components/Gutter' -import { Input } from '../_components/Input' -import classes from './index.module.css' +import { RecoverPasswordForm } from './RecoverPasswordForm' -type FormData = { - email: string -} - -const RecoverPassword: React.FC = () => { - const [error, setError] = useState('') - const [success, setSuccess] = useState(false) - const { forgotPassword } = useAuth() - - const { - register, - handleSubmit, - formState: { errors }, - } = useForm() - - const onSubmit = useCallback( - async (data: FormData) => { - try { - const user = await forgotPassword(data as Parameters[0]) - - if (user) { - setSuccess(true) - setError('') - } - } catch (err: any) { - setError(err?.message || 'An error occurred while attempting to recover password.') - } - }, - [forgotPassword], - ) +import classes from './index.module.scss' +export default async function RecoverPassword() { return ( - - {!success && ( - -

Recover Password

-

- Please enter your email below. You will receive an email message with instructions on - how to reset your password. -

- {error &&
{error}
} -
- - -
-
- )} - {success && ( - -

Request submitted

-

Check your email for a link that will allow you to securely reset your password.

-
- )} + + ) } - -export default RecoverPassword diff --git a/examples/auth/next-app/app/reset-password/ResetPasswordForm/index.module.scss b/examples/auth/next-app/app/reset-password/ResetPasswordForm/index.module.scss new file mode 100644 index 000000000..2f7734603 --- /dev/null +++ b/examples/auth/next-app/app/reset-password/ResetPasswordForm/index.module.scss @@ -0,0 +1,13 @@ +@import "../../_css/common"; + +.form { + width: 66.66%; + + @include mid-break { + width: 100%; + } +} + +.submit { + margin-top: var(--base); +} diff --git a/examples/auth/next-app/app/reset-password/ResetPasswordForm/index.tsx b/examples/auth/next-app/app/reset-password/ResetPasswordForm/index.tsx new file mode 100644 index 000000000..80e59f731 --- /dev/null +++ b/examples/auth/next-app/app/reset-password/ResetPasswordForm/index.tsx @@ -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() + + 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 ( +
+ + + + - +
) } - -export default ResetPassword diff --git a/examples/auth/next-pages/README.md b/examples/auth/next-pages/README.md index d0628b0e2..a9e740f88 100644 --- a/examples/auth/next-pages/README.md +++ b/examples/auth/next-pages/README.md @@ -1,6 +1,6 @@ # Payload Auth Example Front-End -This is a [Next.js](https://nextjs.org) app using the [Pages Router](https://nextjs.org/docs/pages). It was made explicitly for Payload's [Auth Example](https://github.com/payloadcms/payload/tree/master/examples/auth/cms). +This is a [Payload](https://payloadcms.com) + [Next.js](https://nextjs.org) app using the [Pages Router](https://nextjs.org/docs/pages) made explicitly for the [Payload Auth Example](https://github.com/payloadcms/payload/tree/master/examples/auth). It demonstrates how to authenticate your Next.js app using [Payload Authentication](https://payloadcms.com/docs/authentication/overview). > This example uses the Pages Router, the legacy API of Next.js. If your app is using the latest [App Router](https://nextjs.org/docs/pages), check out the official [App Router Example](https://github.com/payloadcms/payload/tree/master/examples/auth/next-app). @@ -8,7 +8,7 @@ This is a [Next.js](https://nextjs.org) app using the [Pages Router](https://nex ### Payload -First you'll need a running [Payload](https://github.com/payloadcms/payload) app. If you have not done so already, open up the `cms` folder and follow the setup instructions. Take note of your `serverURL`, you'll need this in the next step. +First you'll need a running Payload app. If you have not done so already, clone down the [`cms`](../cms) folder and follow the setup instructions there to get it up and running. This will provide all the necessary APIs that your Next.js app will be using for authentication. ### Next.js @@ -18,20 +18,24 @@ First you'll need a running [Payload](https://github.com/payloadcms/payload) app 4. `yarn dev` or `npm run dev` to start the server 5. `open http://localhost:3001` to see the result -Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Auth Example](https://github.com/payloadcms/payload/tree/master/examples/auth/cms) for full details. +Once running, a user is automatically seeded in your local environment with some basic instructions. See the [Payload Auth Example](https://github.com/payloadcms/payload/tree/master/examples/auth) for full details. ## Learn More -To learn more about PayloadCMS and Next.js, take a look at the following resources: +To learn more about Payload and Next.js, take a look at the following resources: - [Payload Documentation](https://payloadcms.com/docs) - learn about Payload features and API. - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. -You can check out [the Payload GitHub repository](https://github.com/payloadcms/payload/) as well as [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! +You can check out [the Payload GitHub repository](https://github.com/payloadcms/payload) as well as [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! ## Deployment The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new) from the creators of Next.js. You could also combine this app into a [single Express server](https://github.com/payloadcms/payload/tree/master/examples/custom-server) and deploy in to [Payload Cloud](https://payloadcms.com/new/import). Check out our [Payload deployment documentation](https://payloadcms.com/docs/production/deployment) or the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. + +## Questions + +If you have any issues or questions, reach out to us on [Discord](https://discord.com/invite/payload) or start a [GitHub discussion](https://github.com/payloadcms/payload/discussions). diff --git a/examples/auth/next-pages/src/components/Button/index.module.scss b/examples/auth/next-pages/src/components/Button/index.module.scss new file mode 100644 index 000000000..304cdba56 --- /dev/null +++ b/examples/auth/next-pages/src/components/Button/index.module.scss @@ -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); +} diff --git a/examples/auth/next-pages/src/components/Button/index.tsx b/examples/auth/next-pages/src/components/Button/index.tsx new file mode 100644 index 000000000..61130c1a9 --- /dev/null +++ b/examples/auth/next-pages/src/components/Button/index.tsx @@ -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 = ({ + 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 = ( +
+ {label} +
+ ) + + if (onClick || type === 'submit') el = 'button' + + if (el === 'link') { + return ( + + {content} + + ) + } + + const Element: ElementType = el + + return ( + + {content} + + ) +} diff --git a/examples/auth/next-pages/src/components/Gutter/index.module.scss b/examples/auth/next-pages/src/components/Gutter/index.module.scss index 09f9f61be..be9e377a7 100644 --- a/examples/auth/next-pages/src/components/Gutter/index.module.scss +++ b/examples/auth/next-pages/src/components/Gutter/index.module.scss @@ -1,7 +1,7 @@ .gutter { - max-width: var(--max-width); - width: 100%; - margin: auto; + max-width: 1920px; + margin-left: auto; + margin-right: auto; } .gutterLeft { diff --git a/examples/auth/next-pages/src/components/Header/Nav/index.module.scss b/examples/auth/next-pages/src/components/Header/Nav/index.module.scss new file mode 100644 index 000000000..4558716b1 --- /dev/null +++ b/examples/auth/next-pages/src/components/Header/Nav/index.module.scss @@ -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; +} diff --git a/examples/auth/next-pages/src/components/Header/Nav/index.tsx b/examples/auth/next-pages/src/components/Header/Nav/index.tsx new file mode 100644 index 000000000..66a098bee --- /dev/null +++ b/examples/auth/next-pages/src/components/Header/Nav/index.tsx @@ -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 ( + + ) +} diff --git a/examples/auth/next-pages/src/components/Header/index.module.scss b/examples/auth/next-pages/src/components/Header/index.module.scss index c99ebda4c..ee7aa03fd 100644 --- a/examples/auth/next-pages/src/components/Header/index.module.scss +++ b/examples/auth/next-pages/src/components/Header/index.module.scss @@ -1,16 +1,22 @@ +@use '../../css/queries.scss' as *; + .header { padding: var(--base) 0; - z-index: var(--header-z-index); } .wrap { display: flex; justify-content: space-between; + flex-wrap: wrap; + gap: calc(var(--base) / 2) var(--base); } -.nav { - a { - text-decoration: none; - margin-left: var(--base); +.logo { + width: 150px; +} + +:global([data-theme="light"]) { + .logo { + filter: invert(1); } } diff --git a/examples/auth/next-pages/src/components/Header/index.tsx b/examples/auth/next-pages/src/components/Header/index.tsx index 8ac42249c..7b7ebec73 100644 --- a/examples/auth/next-pages/src/components/Header/index.tsx +++ b/examples/auth/next-pages/src/components/Header/index.tsx @@ -1,15 +1,13 @@ -import React, { Fragment } from 'react' +import React from 'react' import Image from 'next/image' import Link from 'next/link' -import { useAuth } from '../Auth' import { Gutter } from '../Gutter' +import { HeaderNav } from './Nav' import classes from './index.module.scss' export const Header: React.FC = () => { - const { user } = useAuth() - return (
@@ -27,20 +25,7 @@ export const Header: React.FC = () => { /> - +
) diff --git a/examples/auth/next-pages/src/components/Input/index.module.css b/examples/auth/next-pages/src/components/Input/index.module.css deleted file mode 100644 index 655128696..000000000 --- a/examples/auth/next-pages/src/components/Input/index.module.css +++ /dev/null @@ -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; -} diff --git a/examples/auth/next-pages/src/components/Input/index.module.scss b/examples/auth/next-pages/src/components/Input/index.module.scss new file mode 100644 index 000000000..ac587ba49 --- /dev/null +++ b/examples/auth/next-pages/src/components/Input/index.module.scss @@ -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; +} diff --git a/examples/auth/next-pages/src/components/Input/index.tsx b/examples/auth/next-pages/src/components/Input/index.tsx index 8d4aaf5b4..771626cf0 100644 --- a/examples/auth/next-pages/src/components/Input/index.tsx +++ b/examples/auth/next-pages/src/components/Input/index.tsx @@ -1,7 +1,7 @@ import React from 'react' -import { FieldValues, UseFormRegister } from 'react-hook-form' +import { FieldValues, UseFormRegister, Validate } from 'react-hook-form' -import classes from './index.module.css' +import classes from './index.module.scss' type Props = { name: string @@ -9,7 +9,8 @@ type Props = { register: UseFormRegister required?: boolean error: any - type?: 'text' | 'number' | 'password' + type?: 'text' | 'number' | 'password' | 'email' + validate?: (value: string) => boolean | string } export const Input: React.FC = ({ @@ -19,14 +20,36 @@ export const Input: React.FC = ({ register, error, type = 'text', + validate, }) => { return ( -
+
- - {error &&
This field is required
} + + {error && ( +
+ {!error?.message && error?.type === 'required' + ? 'This field is required' + : error?.message} +
+ )}
) } diff --git a/examples/auth/next-pages/src/components/Message/index.module.scss b/examples/auth/next-pages/src/components/Message/index.module.scss new file mode 100644 index 000000000..2ee676056 --- /dev/null +++ b/examples/auth/next-pages/src/components/Message/index.module.scss @@ -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); + } +} diff --git a/examples/auth/next-pages/src/components/Message/index.tsx b/examples/auth/next-pages/src/components/Message/index.tsx new file mode 100644 index 000000000..3cd806086 --- /dev/null +++ b/examples/auth/next-pages/src/components/Message/index.tsx @@ -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 ( +
+ {messageToRender} +
+ ) + } + return null +} diff --git a/examples/auth/next-pages/src/components/RenderParams/index.tsx b/examples/auth/next-pages/src/components/RenderParams/index.tsx new file mode 100644 index 000000000..2abb2c436 --- /dev/null +++ b/examples/auth/next-pages/src/components/RenderParams/index.tsx @@ -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 ( +
+ {paramValues.map(paramValue => ( + + ))} +
+ ) + } + + return null +} diff --git a/examples/auth/next-pages/src/css/app.scss b/examples/auth/next-pages/src/css/app.scss new file mode 100644 index 000000000..fcf6af756 --- /dev/null +++ b/examples/auth/next-pages/src/css/app.scss @@ -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; +} diff --git a/examples/auth/next-pages/src/css/colors.scss b/examples/auth/next-pages/src/css/colors.scss new file mode 100644 index 000000000..68bcbc2d5 --- /dev/null +++ b/examples/auth/next-pages/src/css/colors.scss @@ -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); +} diff --git a/examples/auth/next-pages/src/css/common.scss b/examples/auth/next-pages/src/css/common.scss new file mode 100644 index 000000000..bebb9f3aa --- /dev/null +++ b/examples/auth/next-pages/src/css/common.scss @@ -0,0 +1 @@ +@forward './queries.scss'; diff --git a/examples/auth/next-pages/src/css/queries.scss b/examples/auth/next-pages/src/css/queries.scss new file mode 100644 index 000000000..8f84ac709 --- /dev/null +++ b/examples/auth/next-pages/src/css/queries.scss @@ -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; + } +} diff --git a/examples/auth/next-pages/src/css/theme.scss b/examples/auth/next-pages/src/css/theme.scss new file mode 100644 index 000000000..0c93d334f --- /dev/null +++ b/examples/auth/next-pages/src/css/theme.scss @@ -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); + } + } + } +} diff --git a/examples/auth/next-pages/src/css/type.scss b/examples/auth/next-pages/src/css/type.scss new file mode 100644 index 000000000..f8d1d0716 --- /dev/null +++ b/examples/auth/next-pages/src/css/type.scss @@ -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; + } +} diff --git a/examples/auth/next-pages/src/pages/_app.tsx b/examples/auth/next-pages/src/pages/_app.tsx index b08c429e8..efcddc0da 100644 --- a/examples/auth/next-pages/src/pages/_app.tsx +++ b/examples/auth/next-pages/src/pages/_app.tsx @@ -1,26 +1,24 @@ import type { AppProps } from 'next/app' -import { AuthProvider } from '../components/Auth' import { Header } from '../components/Header' +import { AuthProvider } from '../providers/Auth' -import './app.scss' - -import classes from './index.module.scss' +import '../css/app.scss' export default function MyApp({ Component, pageProps }: AppProps) { return ( - // The `AuthProvider` can be used with either REST or GraphQL APIs - // Just change the `api` prop to "graphql" or "rest", that's it! - +
-
- {/* typescript flags this `@ts-expect-error` declaration as unneeded, but types are breaking the build process + {/* typescript flags this `@ts-expect-error` declaration as unneeded, but types are breaking the build process Remove these comments when the issue is resolved See more here: https://github.com/facebook/react/issues/24304 */} - {/* @ts-expect-error */} - -
+ {/* @ts-expect-error */} + ) } diff --git a/examples/auth/next-pages/src/pages/account/index.module.css b/examples/auth/next-pages/src/pages/account/index.module.css deleted file mode 100644 index 9a5decbdd..000000000 --- a/examples/auth/next-pages/src/pages/account/index.module.css +++ /dev/null @@ -1,17 +0,0 @@ -.form { - margin-bottom: 30px; -} - -.success, -.error, -.message { - margin-bottom: 30px; -} - -.success { - color: green; -} - -.error { - color: red; -} diff --git a/examples/auth/next-pages/src/pages/account/index.module.scss b/examples/auth/next-pages/src/pages/account/index.module.scss new file mode 100644 index 000000000..9d9b9d879 --- /dev/null +++ b/examples/auth/next-pages/src/pages/account/index.module.scss @@ -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); +} diff --git a/examples/auth/next-pages/src/pages/account/index.tsx b/examples/auth/next-pages/src/pages/account/index.tsx index b2678c71b..f161dd1c5 100644 --- a/examples/auth/next-pages/src/pages/account/index.tsx +++ b/examples/auth/next-pages/src/pages/account/index.tsx @@ -1,32 +1,42 @@ -import React, { useCallback, useEffect, useState } from 'react' +import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react' import { useForm } from 'react-hook-form' import Link from 'next/link' import { useRouter } from 'next/router' -import { useAuth } from '../../components/Auth' +import { Button } from '../../components/Button' import { Gutter } from '../../components/Gutter' import { Input } from '../../components/Input' -import classes from './index.module.css' +import { Message } from '../../components/Message' +import { RenderParams } from '../../components/RenderParams' +import { useAuth } from '../../providers/Auth' + +import classes from './index.module.scss' type FormData = { email: string - firstName: string - lastName: string + name: string + password: string + passwordConfirm: string } const Account: React.FC = () => { const [error, setError] = useState('') const [success, setSuccess] = useState('') const { user, setUser } = useAuth() + const [changePassword, setChangePassword] = useState(false) const router = useRouter() const { register, handleSubmit, - formState: { errors }, + formState: { errors, isLoading }, reset, + watch, } = useForm() + const password = useRef({}) + password.current = watch('password', '') + const onSubmit = useCallback( async (data: FormData) => { if (user) { @@ -42,21 +52,22 @@ const Account: React.FC = () => { if (response.ok) { const json = await response.json() - - // Update the user in auth state with new values setUser(json.doc) - - // Set success message for user setSuccess('Successfully updated account.') - - // Clear any existing errors setError('') + setChangePassword(false) + reset({ + email: json.doc.email, + name: json.doc.name, + password: '', + passwordConfirm: '', + }) } else { setError('There was a problem updating your account.') } } }, - [user, setUser], + [user, setUser, reset], ) useEffect(() => { @@ -68,37 +79,87 @@ const Account: React.FC = () => { if (user) { reset({ email: user.email, - firstName: user.firstName, - lastName: user.lastName, + password: '', + passwordConfirm: '', }) } - }, [user, reset, router]) - - useEffect(() => { - if (typeof router.query.success === 'string') { - setSuccess(router.query.success) - } - }, [router]) + }, [user, router, reset, changePassword]) return ( - + +

Account

- {router.query.message &&
{router.query.message}
} - {error &&
{error}
} - {success &&
{success}
} +

+ {`This is your account dashboard. Here you can update your account information and more. To manage all users, `} + + login to the admin dashboard + + {'.'} +

- + {!changePassword ? ( + +

+ {'Change your account details below, or '} + + {' to change your password.'} +

+ +
+ ) : ( + +

+ {'Change your password below, or '} + + . +

+ + value === password.current || 'The passwords do not match'} + error={errors.passwordConfirm} + /> +
+ )} +
- Log out + - -

- {'Already have an account? '} - Login -

- - )} - {success && ( - -

Account created successfully

-

You are now logged in.

- Go to your account -
- )} + +

Create Account

+ +
+

+ {`This is where new customers can signup and create a new account. To manage all users, `} + + login to the admin dashboard + + {'.'} +

+ + + + value === password.current || 'The passwords do not match'} + error={errors.passwordConfirm} + /> + - +
+

+ {`Please enter your email below. You will receive an email message with instructions on + how to reset your password. To manage your all users, `} + + login to the admin dashboard + + {'.'} +

+
+ + +
)} {success && ( diff --git a/examples/auth/next-pages/src/pages/reset-password/index.module.css b/examples/auth/next-pages/src/pages/reset-password/index.module.css deleted file mode 100644 index 7d2f0a0af..000000000 --- a/examples/auth/next-pages/src/pages/reset-password/index.module.css +++ /dev/null @@ -1,4 +0,0 @@ -.error { - color: red; - margin-bottom: 30px; -} diff --git a/examples/auth/next-pages/src/pages/reset-password/index.module.scss b/examples/auth/next-pages/src/pages/reset-password/index.module.scss new file mode 100644 index 000000000..26f7c2479 --- /dev/null +++ b/examples/auth/next-pages/src/pages/reset-password/index.module.scss @@ -0,0 +1,17 @@ +@import "../../css/common"; + +.resetPassword { + margin-bottom: var(--block-padding); +} + +.form { + width: 66.66%; + + @include mid-break { + width: 100%; + } +} + +.submit { + margin-top: var(--base); +} diff --git a/examples/auth/next-pages/src/pages/reset-password/index.tsx b/examples/auth/next-pages/src/pages/reset-password/index.tsx index 1030e5bb5..22bde59c6 100644 --- a/examples/auth/next-pages/src/pages/reset-password/index.tsx +++ b/examples/auth/next-pages/src/pages/reset-password/index.tsx @@ -1,11 +1,14 @@ -import React, { useCallback, useEffect, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import { useForm } from 'react-hook-form' import { useRouter } from 'next/router' -import { useAuth } from '../../components/Auth' +import { Button } from '../../components/Button' import { Gutter } from '../../components/Gutter' import { Input } from '../../components/Input' -import classes from './index.module.css' +import { Message } from '../../components/Message' +import { useAuth } from '../../providers/Auth' + +import classes from './index.module.scss' type FormData = { password: string @@ -14,10 +17,10 @@ type FormData = { const ResetPassword: React.FC = () => { const [error, setError] = useState('') - const { login, resetPassword } = useAuth() + const { login } = useAuth() const router = useRouter() - - const token = typeof router.query.token === 'string' ? router.query.token : undefined + const searchParams = useMemo(() => new URLSearchParams(router.query as any), [router.query]) + const token = searchParams.get('token') const { register, @@ -28,33 +31,41 @@ const ResetPassword: React.FC = () => { const onSubmit = useCallback( async (data: FormData) => { - try { - const user = await resetPassword(data as Parameters[0]) + 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 (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) { - setError(err?.message || 'An error occurred while attempting to reset password.') + 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, resetPassword], + [router, login], ) - // When Next.js populates token within router, reset form with new token value + // when Next.js populates token within router, + // reset form with new token value useEffect(() => { - reset({ token }) + reset({ token: token || undefined }) }, [reset, token]) return ( - +

Reset Password

Please enter a new password below.

- {error &&
{error}
} -
+ + { error={errors.password} /> - +