Accounts system, no RBAC yet
This commit is contained in:
@ -13,6 +13,7 @@
|
||||
"dependencies": {
|
||||
"@mdi/js": "^7.3.67",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@redwoodjs/auth-dbauth-web": "6.3.2",
|
||||
"@redwoodjs/forms": "6.3.2",
|
||||
"@redwoodjs/router": "6.3.2",
|
||||
"@redwoodjs/web": "6.3.2",
|
||||
|
@ -4,15 +4,19 @@ import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
|
||||
import FatalErrorPage from 'src/pages/FatalErrorPage'
|
||||
import Routes from 'src/Routes'
|
||||
|
||||
import { AuthProvider, useAuth } from './auth'
|
||||
|
||||
import './scaffold.css'
|
||||
import './index.css'
|
||||
|
||||
const App = () => (
|
||||
<FatalErrorBoundary page={FatalErrorPage}>
|
||||
<RedwoodProvider titleTemplate="%PageTitle | %AppTitle">
|
||||
<RedwoodApolloProvider>
|
||||
<Routes />
|
||||
</RedwoodApolloProvider>
|
||||
<AuthProvider>
|
||||
<RedwoodApolloProvider useAuth={useAuth}>
|
||||
<Routes />
|
||||
</RedwoodApolloProvider>
|
||||
</AuthProvider>
|
||||
</RedwoodProvider>
|
||||
</FatalErrorBoundary>
|
||||
)
|
||||
|
@ -7,20 +7,28 @@
|
||||
// 'src/pages/HomePage/HomePage.js' -> HomePage
|
||||
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
|
||||
|
||||
import { Router, Route, Set } from '@redwoodjs/router'
|
||||
import { Router, Route, Set, Private } from '@redwoodjs/router'
|
||||
|
||||
import NavbarLayout from 'src/layouts/NavbarLayout'
|
||||
import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
|
||||
|
||||
import { useAuth } from './auth'
|
||||
|
||||
const Routes = () => {
|
||||
return (
|
||||
<Router>
|
||||
<Set wrap={ScaffoldLayout} title="Parts" titleTo="parts" buttonLabel="New Part" buttonTo="newPart">
|
||||
<Route path="/admin/parts/new" page={PartNewPartPage} name="newPart" />
|
||||
<Route path="/admin/parts/{id:Int}/edit" page={PartEditPartPage} name="editPart" />
|
||||
<Route path="/admin/parts/{id:Int}" page={PartPartPage} name="part" />
|
||||
<Route path="/admin/parts" page={PartPartsPage} name="parts" />
|
||||
</Set>
|
||||
<Router useAuth={useAuth}>
|
||||
<Route path="/login" page={LoginPage} name="login" />
|
||||
<Route path="/signup" page={SignupPage} name="signup" />
|
||||
<Route path="/forgot-password" page={ForgotPasswordPage} name="forgotPassword" />
|
||||
<Route path="/reset-password" page={ResetPasswordPage} name="resetPassword" />
|
||||
<Private unauthenticated="home">
|
||||
<Set wrap={ScaffoldLayout} title="Parts" titleTo="parts" buttonLabel="New Part" buttonTo="newPart">
|
||||
<Route path="/admin/parts/new" page={PartNewPartPage} name="newPart" />
|
||||
<Route path="/admin/parts/{id:Int}/edit" page={PartEditPartPage} name="editPart" />
|
||||
<Route path="/admin/parts/{id:Int}" page={PartPartPage} name="part" />
|
||||
<Route path="/admin/parts" page={PartPartsPage} name="parts" />
|
||||
</Set>
|
||||
</Private>
|
||||
<Set wrap={NavbarLayout}>
|
||||
<Route path="/" page={HomePage} name="home" />
|
||||
</Set>
|
||||
|
5
web/src/auth.ts
Normal file
5
web/src/auth.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { createDbAuthClient, createAuth } from '@redwoodjs/auth-dbauth-web'
|
||||
|
||||
const dbAuthClient = createDbAuthClient()
|
||||
|
||||
export const { AuthProvider, useAuth } = createAuth(dbAuthClient)
|
@ -0,0 +1,25 @@
|
||||
// Pass props to your component by passing an `args` object to your story
|
||||
//
|
||||
// ```tsx
|
||||
// export const Primary: Story = {
|
||||
// args: {
|
||||
// propName: propValue
|
||||
// }
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// See https://storybook.js.org/docs/react/writing-stories/args.
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import NavbarAccountIcon from './NavbarAccountIcon'
|
||||
|
||||
const meta: Meta<typeof NavbarAccountIcon> = {
|
||||
component: NavbarAccountIcon,
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof NavbarAccountIcon>
|
||||
|
||||
export const Primary: Story = {}
|
@ -0,0 +1,14 @@
|
||||
import { render } from '@redwoodjs/testing/web'
|
||||
|
||||
import NavbarAccountIcon from './NavbarAccountIcon'
|
||||
|
||||
// Improve this test with help from the Redwood Testing Doc:
|
||||
// https://redwoodjs.com/docs/testing#testing-components
|
||||
|
||||
describe('NavbarAccountIcon', () => {
|
||||
it('renders successfully', () => {
|
||||
expect(() => {
|
||||
render(<NavbarAccountIcon />)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
47
web/src/components/NavbarAccountIcon/NavbarAccountIcon.tsx
Normal file
47
web/src/components/NavbarAccountIcon/NavbarAccountIcon.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { mdiAccount, mdiLogout, mdiLogin } from '@mdi/js'
|
||||
import Icon from '@mdi/react'
|
||||
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
|
||||
import { useAuth } from 'src/auth'
|
||||
|
||||
interface Props {
|
||||
mobile: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const NavbarAccountIcon = ({ mobile, className }: Props) => {
|
||||
const { isAuthenticated, currentUser, logOut } = useAuth()
|
||||
|
||||
return isAuthenticated ? (
|
||||
<div className={className}>
|
||||
<details
|
||||
className={`dropdown dropdown-end ${
|
||||
mobile ? 'space-y-2' : 'space-y-4'
|
||||
}`}
|
||||
>
|
||||
<summary className="btn btn-ghost swap swap-rotate w-12 hover:shadow-lg">
|
||||
<Icon path={mdiAccount} className="h-8 w-8 text-gray-500" />
|
||||
</summary>
|
||||
<div className="dropdown-content flex w-auto flex-row items-center space-x-3 rounded-xl bg-base-100 p-3 shadow-lg">
|
||||
<p className="whitespace-nowrap font-inter text-lg">
|
||||
Hello, {currentUser.firstName}!
|
||||
</p>
|
||||
<button className="btn btn-ghost" type="button" onClick={logOut}>
|
||||
<Icon path={mdiLogout} className="h-7 w-7 text-gray-600" />
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
) : (
|
||||
<div className={className}>
|
||||
<Link to={routes.login()}>
|
||||
<button className="btn btn-ghost" type="button" onClick={logOut}>
|
||||
<Icon path={mdiLogin} className="h-8 w-8 text-gray-500" />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NavbarAccountIcon
|
@ -3,6 +3,7 @@ import Icon from '@mdi/react'
|
||||
|
||||
import { Link, routes } from '@redwoodjs/router'
|
||||
|
||||
import NavbarAccountIcon from 'src/components/NavbarAccountIcon/NavbarAccountIcon'
|
||||
import ThemeToggle from 'src/components/ThemeToggle/ThemeToggle'
|
||||
|
||||
type NavBarLayoutProps = {
|
||||
@ -14,7 +15,10 @@ const NavBarLayout = ({ children }: NavBarLayoutProps) => {
|
||||
<>
|
||||
<div className="navbar bg-base-100 shadow-lg">
|
||||
<div className="justify-start space-x-3">
|
||||
<Icon path={mdiChip} className="ml-3 h-10 text-logo" />
|
||||
<Icon
|
||||
path={mdiChip}
|
||||
className="ml-3 hidden h-10 text-logo md:block"
|
||||
/>
|
||||
<Link
|
||||
to={routes.home()}
|
||||
className="btn btn-ghost items-center hover:shadow-lg"
|
||||
@ -36,6 +40,7 @@ const NavBarLayout = ({ children }: NavBarLayoutProps) => {
|
||||
</li>
|
||||
</ul> */}
|
||||
<ThemeToggle />
|
||||
<NavbarAccountIcon mobile={false} className="hidden lg:block" />
|
||||
<div className="lg:hidden">
|
||||
<input
|
||||
id="mobile-menu-drawer"
|
||||
@ -57,7 +62,7 @@ const NavBarLayout = ({ children }: NavBarLayoutProps) => {
|
||||
</label>
|
||||
<ul className="min-h-full w-80 space-y-3 bg-base-100 p-3 text-base-content shadow-lg">
|
||||
<li>
|
||||
<div className="flex justify-center space-x-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Icon path={mdiChip} className="ml-3 h-10 text-logo" />
|
||||
<Link
|
||||
to={routes.home()}
|
||||
@ -67,6 +72,7 @@ const NavBarLayout = ({ children }: NavBarLayoutProps) => {
|
||||
Parts Inventory
|
||||
</p>
|
||||
</Link>
|
||||
<NavbarAccountIcon mobile={true} />
|
||||
</div>
|
||||
</li>
|
||||
{/* <li>
|
||||
|
96
web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
Normal file
96
web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
import { Form, Label, TextField, Submit, FieldError } from '@redwoodjs/forms'
|
||||
import { navigate, routes } from '@redwoodjs/router'
|
||||
import { MetaTags } from '@redwoodjs/web'
|
||||
import { toast, Toaster } from '@redwoodjs/web/toast'
|
||||
|
||||
import { useAuth } from 'src/auth'
|
||||
|
||||
const ForgotPasswordPage = () => {
|
||||
const { isAuthenticated, forgotPassword } = useAuth()
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
navigate(routes.home())
|
||||
}
|
||||
}, [isAuthenticated])
|
||||
|
||||
const emailRef = useRef<HTMLInputElement>(null)
|
||||
useEffect(() => {
|
||||
emailRef?.current?.focus()
|
||||
}, [])
|
||||
|
||||
const onSubmit = async (data: { email: string }) => {
|
||||
const response = await forgotPassword(data.email)
|
||||
|
||||
if (response.error) {
|
||||
toast.error(response.error)
|
||||
} else {
|
||||
// The function `forgotPassword.handler` in api/src/functions/auth.js has
|
||||
// been invoked, let the user know how to get the link to reset their
|
||||
// password (sent in email, perhaps?)
|
||||
toast.success(
|
||||
'A link to reset your password was sent to ' + response.email
|
||||
)
|
||||
navigate(routes.login())
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<MetaTags title="Forgot Password" />
|
||||
|
||||
<main className="rw-main">
|
||||
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
|
||||
<div className="rw-scaffold rw-login-container">
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">
|
||||
Forgot Password
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<div className="rw-segment-main">
|
||||
<div className="rw-form-wrapper">
|
||||
<Form onSubmit={onSubmit} className="rw-form-wrapper">
|
||||
<div className="text-left">
|
||||
<Label
|
||||
name="email"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Email
|
||||
</Label>
|
||||
<TextField
|
||||
name="email"
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
ref={emailRef}
|
||||
validation={{
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Email is required',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<FieldError name="email" className="rw-field-error" />
|
||||
</div>
|
||||
|
||||
<div className="rw-button-group">
|
||||
<Submit className="btn btn-primary font-inter">
|
||||
Submit
|
||||
</Submit>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ForgotPasswordPage
|
136
web/src/pages/LoginPage/LoginPage.tsx
Normal file
136
web/src/pages/LoginPage/LoginPage.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import { useRef } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import {
|
||||
Form,
|
||||
Label,
|
||||
TextField,
|
||||
PasswordField,
|
||||
Submit,
|
||||
FieldError,
|
||||
} from '@redwoodjs/forms'
|
||||
import { Link, navigate, routes } from '@redwoodjs/router'
|
||||
import { MetaTags } from '@redwoodjs/web'
|
||||
import { toast, Toaster } from '@redwoodjs/web/toast'
|
||||
|
||||
import { useAuth } from 'src/auth'
|
||||
|
||||
const LoginPage = () => {
|
||||
const { isAuthenticated, logIn } = useAuth()
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
navigate(routes.home())
|
||||
}
|
||||
}, [isAuthenticated])
|
||||
|
||||
const emailRef = useRef<HTMLInputElement>(null)
|
||||
useEffect(() => {
|
||||
emailRef.current?.focus()
|
||||
}, [])
|
||||
|
||||
const onSubmit = async (data: Record<string, string>) => {
|
||||
const response = await logIn({
|
||||
username: data.email,
|
||||
password: data.password,
|
||||
})
|
||||
|
||||
if (response.message) {
|
||||
toast(response.message)
|
||||
} else if (response.error) {
|
||||
toast.error(response.error)
|
||||
} else {
|
||||
toast.success('Welcome back!')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<MetaTags title="Login" />
|
||||
|
||||
<main className="rw-main">
|
||||
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
|
||||
<div className="rw-scaffold rw-login-container">
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">Login</h2>
|
||||
</header>
|
||||
|
||||
<div className="rw-segment-main">
|
||||
<div className="rw-form-wrapper">
|
||||
<Form onSubmit={onSubmit} className="rw-form-wrapper">
|
||||
<Label
|
||||
name="email"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Email
|
||||
</Label>
|
||||
<TextField
|
||||
name="email"
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
ref={emailRef}
|
||||
validation={{
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Required',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<FieldError name="email" className="rw-field-error" />
|
||||
|
||||
<Label
|
||||
name="password"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Password
|
||||
</Label>
|
||||
<PasswordField
|
||||
name="password"
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
autoComplete="current-password"
|
||||
validation={{
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Required',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<FieldError name="password" className="rw-field-error" />
|
||||
|
||||
<div className="rw-forgot-link">
|
||||
<Link
|
||||
to={routes.forgotPassword()}
|
||||
className="rw-forgot-link"
|
||||
>
|
||||
Forgot Password?
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="rw-button-group">
|
||||
<Submit className="btn btn-primary font-inter">
|
||||
Login
|
||||
</Submit>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rw-login-link">
|
||||
<span className="font-inter">Don't have an account?</span>{' '}
|
||||
<Link to={routes.signup()} className="rw-link">
|
||||
Sign up!
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginPage
|
121
web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx
Normal file
121
web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
import {
|
||||
Form,
|
||||
Label,
|
||||
PasswordField,
|
||||
Submit,
|
||||
FieldError,
|
||||
} from '@redwoodjs/forms'
|
||||
import { navigate, routes } from '@redwoodjs/router'
|
||||
import { MetaTags } from '@redwoodjs/web'
|
||||
import { toast, Toaster } from '@redwoodjs/web/toast'
|
||||
|
||||
import { useAuth } from 'src/auth'
|
||||
|
||||
const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
|
||||
const { isAuthenticated, reauthenticate, validateResetToken, resetPassword } =
|
||||
useAuth()
|
||||
const [enabled, setEnabled] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
navigate(routes.home())
|
||||
}
|
||||
}, [isAuthenticated])
|
||||
|
||||
useEffect(() => {
|
||||
const validateToken = async () => {
|
||||
const response = await validateResetToken(resetToken)
|
||||
if (response.error) {
|
||||
setEnabled(false)
|
||||
toast.error(response.error)
|
||||
} else {
|
||||
setEnabled(true)
|
||||
}
|
||||
}
|
||||
validateToken()
|
||||
}, [resetToken, validateResetToken])
|
||||
|
||||
const passwordRef = useRef<HTMLInputElement>(null)
|
||||
useEffect(() => {
|
||||
passwordRef.current?.focus()
|
||||
}, [])
|
||||
|
||||
const onSubmit = async (data: Record<string, string>) => {
|
||||
const response = await resetPassword({
|
||||
resetToken,
|
||||
password: data.password,
|
||||
})
|
||||
|
||||
if (response.error) {
|
||||
toast.error(response.error)
|
||||
} else {
|
||||
toast.success('Password changed!')
|
||||
await reauthenticate()
|
||||
navigate(routes.login())
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<MetaTags title="Reset Password" />
|
||||
|
||||
<main className="rw-main">
|
||||
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
|
||||
<div className="rw-scaffold rw-login-container">
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">
|
||||
Reset Password
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<div className="rw-segment-main">
|
||||
<div className="rw-form-wrapper">
|
||||
<Form onSubmit={onSubmit} className="rw-form-wrapper">
|
||||
<div className="text-left">
|
||||
<Label
|
||||
name="password"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
New Password
|
||||
</Label>
|
||||
<PasswordField
|
||||
name="password"
|
||||
autoComplete="new-password"
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
disabled={!enabled}
|
||||
ref={passwordRef}
|
||||
validation={{
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Required',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<FieldError name="password" className="rw-field-error" />
|
||||
</div>
|
||||
|
||||
<div className="rw-button-group">
|
||||
<Submit
|
||||
className="btn btn-primary font-inter"
|
||||
disabled={!enabled}
|
||||
>
|
||||
Submit
|
||||
</Submit>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResetPasswordPage
|
182
web/src/pages/SignupPage/SignupPage.tsx
Normal file
182
web/src/pages/SignupPage/SignupPage.tsx
Normal file
@ -0,0 +1,182 @@
|
||||
import { useRef } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import {
|
||||
Form,
|
||||
Label,
|
||||
TextField,
|
||||
PasswordField,
|
||||
FieldError,
|
||||
Submit,
|
||||
} from '@redwoodjs/forms'
|
||||
import { Link, navigate, routes } from '@redwoodjs/router'
|
||||
import { MetaTags } from '@redwoodjs/web'
|
||||
import { toast, Toaster } from '@redwoodjs/web/toast'
|
||||
|
||||
import { useAuth } from 'src/auth'
|
||||
|
||||
const SignupPage = () => {
|
||||
const { isAuthenticated, signUp } = useAuth()
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
navigate(routes.home())
|
||||
}
|
||||
}, [isAuthenticated])
|
||||
|
||||
// focus on name box on page load
|
||||
const nameRef = useRef<HTMLInputElement>(null)
|
||||
useEffect(() => {
|
||||
nameRef.current?.focus()
|
||||
}, [])
|
||||
|
||||
const onSubmit = async (data: Record<string, string>) => {
|
||||
console.log(data)
|
||||
const response = await signUp({
|
||||
username: data.email,
|
||||
password: data.password,
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
})
|
||||
|
||||
if (response.message) {
|
||||
toast(response.message)
|
||||
} else if (response.error) {
|
||||
toast.error(response.error)
|
||||
} else {
|
||||
// user is signed in automatically
|
||||
toast.success('Welcome!')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<MetaTags title="Signup" />
|
||||
|
||||
<main>
|
||||
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
|
||||
<div className="rw-scaffold rw-login-container">
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">Signup</h2>
|
||||
</header>
|
||||
|
||||
<div className="rw-segment-main">
|
||||
<div className="rw-form-wrapper">
|
||||
<Form onSubmit={onSubmit} className="rw-form-wrapper">
|
||||
<div className="flex justify-between space-x-3">
|
||||
<div>
|
||||
<Label
|
||||
name="firstName"
|
||||
className="mt-6 block text-left font-semibold text-gray-600"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
First Name
|
||||
</Label>
|
||||
<TextField
|
||||
name="firstName"
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
ref={nameRef}
|
||||
validation={{
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Required',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<FieldError name="firstName" className="rw-field-error" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label
|
||||
name="lastName"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Last Name
|
||||
</Label>
|
||||
<TextField
|
||||
name="lastName"
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
validation={{
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Required',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<FieldError name="lastName" className="rw-field-error" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Label
|
||||
name="email"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Email
|
||||
</Label>
|
||||
<TextField
|
||||
name="email"
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
validation={{
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Required',
|
||||
},
|
||||
pattern: {
|
||||
value: new RegExp(
|
||||
/^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-]+)(\.[a-zA-Z]{2,5}){1,2}$/
|
||||
),
|
||||
message: 'Email is not valid',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<FieldError name="email" className="rw-field-error" />
|
||||
|
||||
<Label
|
||||
name="password"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Password
|
||||
</Label>
|
||||
<PasswordField
|
||||
name="password"
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
autoComplete="current-password"
|
||||
validation={{
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Required',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<FieldError name="password" className="rw-field-error" />
|
||||
|
||||
<div className="rw-button-group">
|
||||
<Submit className="btn btn-primary font-inter">
|
||||
Sign Up
|
||||
</Submit>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rw-login-link">
|
||||
<span className="font-inter">Already have an account?</span>{' '}
|
||||
<Link to={routes.login()} className="rw-link">
|
||||
Log in!
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SignupPage
|
@ -46,19 +46,19 @@
|
||||
@apply bg-gray-100 p-4;
|
||||
}
|
||||
.rw-link {
|
||||
@apply text-blue-400 underline;
|
||||
@apply text-blue-400 underline font-inter;
|
||||
}
|
||||
.rw-link:hover {
|
||||
@apply text-blue-500;
|
||||
}
|
||||
.rw-forgot-link {
|
||||
@apply mt-1 text-right text-xs text-gray-400 underline;
|
||||
@apply mt-1 text-right text-xs text-gray-400 underline font-inter;
|
||||
}
|
||||
.rw-forgot-link:hover {
|
||||
@apply text-blue-500;
|
||||
}
|
||||
.rw-heading {
|
||||
@apply font-semibold;
|
||||
@apply font-semibold font-inter;
|
||||
}
|
||||
.rw-heading.rw-heading-primary {
|
||||
@apply text-xl;
|
||||
@ -89,32 +89,11 @@
|
||||
@apply mt-2 list-inside list-disc;
|
||||
}
|
||||
.rw-button {
|
||||
@apply flex cursor-pointer justify-center rounded border-0 bg-gray-200 px-4 py-1 text-xs font-semibold uppercase leading-loose tracking-wide text-gray-500 no-underline transition duration-100;
|
||||
}
|
||||
.rw-button:hover {
|
||||
@apply bg-gray-500 text-white;
|
||||
@apply btn btn-primary font-inter
|
||||
}
|
||||
.rw-button.rw-button-small {
|
||||
@apply rounded-sm px-2 py-1 text-xs;
|
||||
}
|
||||
.rw-button.rw-button-green {
|
||||
@apply bg-green-500 text-white;
|
||||
}
|
||||
.rw-button.rw-button-green:hover {
|
||||
@apply bg-green-700;
|
||||
}
|
||||
.rw-button.rw-button-blue {
|
||||
@apply bg-blue-500 text-white;
|
||||
}
|
||||
.rw-button.rw-button-blue:hover {
|
||||
@apply bg-blue-700;
|
||||
}
|
||||
.rw-button.rw-button-red {
|
||||
@apply bg-red-500 text-white;
|
||||
}
|
||||
.rw-button.rw-button-red:hover {
|
||||
@apply bg-red-700 text-white;
|
||||
}
|
||||
.rw-button-icon {
|
||||
@apply mr-1 text-xl leading-5;
|
||||
}
|
||||
@ -128,13 +107,13 @@
|
||||
@apply mt-8;
|
||||
}
|
||||
.rw-label {
|
||||
@apply mt-6 block text-left font-semibold text-gray-600;
|
||||
@apply mt-6 block text-left font-semibold text-gray-600 font-inter;
|
||||
}
|
||||
.rw-label.rw-label-error {
|
||||
@apply text-red-600;
|
||||
}
|
||||
.rw-input {
|
||||
@apply mt-2 block w-full rounded border border-gray-200 bg-white p-2 outline-none;
|
||||
@apply mt-2 block w-full rounded border border-gray-200 bg-white p-2 outline-none font-inter;
|
||||
}
|
||||
.rw-check-radio-items {
|
||||
@apply flex justify-items-center;
|
||||
@ -157,7 +136,7 @@
|
||||
box-shadow: 0 0 5px #c53030;
|
||||
}
|
||||
.rw-field-error {
|
||||
@apply mt-1 block text-xs font-semibold uppercase text-red-600;
|
||||
@apply mt-1 block text-xs font-semibold text-red-600 font-inter text-left;
|
||||
}
|
||||
.rw-table-wrapper-responsive {
|
||||
@apply overflow-x-auto;
|
||||
|
Reference in New Issue
Block a user