issue-129 Update signin/up ui-ux

Getting rid of the netlify widgit and rolling our own, gives us the
flexibility to also add the username into the signup process as well
as allow the user to opt into the newsletter at the same time.

Auth is still netlify, via their "gotrue", we're just handling the more
of it.
This commit is contained in:
Kurt Hutten
2020-12-05 18:16:30 +11:00
parent 46e370531f
commit d3d73ca475
20 changed files with 535 additions and 21 deletions

View File

@@ -62,12 +62,12 @@ export const handler = async (req, _context) => {
db.user.findOne({
where: { userName: seed },
})
const userNameSeed = enforceAlphaNumeric(email.split('@')[0])
const userNameSeed = enforceAlphaNumeric(user?.user_metadata?.userName)
const userName = await generateUniqueString(userNameSeed, isUniqueCallback) // TODO maybe come up with a better default userName?
const input = {
email,
userName,
name: user.user_metadata && user.user_metadata.full_name,
name: user?.user_metadata?.full_name,
id: user.id,
}
await createUserInsecure({ input })

View File

@@ -14,7 +14,7 @@
},
"dependencies": {
"@material-ui/core": "^4.11.0",
"@redwoodjs/auth": "^0.20.0",
"@redwoodjs/auth": "^0.21.0",
"@redwoodjs/forms": "^0.20.0",
"@redwoodjs/router": "^0.20.0",
"@redwoodjs/web": "^0.20.0",
@@ -22,6 +22,7 @@
"controlkit": "^0.1.9",
"get-active-classes": "^0.0.11",
"golden-layout": "^1.5.9",
"gotrue-js": "^0.9.27",
"jquery": "^3.5.1",
"monaco-editor": "^0.20.0",
"monaco-editor-webpack-plugin": "^1.9.1",

View File

@@ -32,6 +32,8 @@ const Routes = () => {
)
return (
<Router>
<Route path="/account-recovery/update-password" page={UpdatePasswordPage} name="updatePassword" />
<Route path="/account-recovery" page={AccountRecoveryPage} name="accountRecovery" />
<Route path="/" page={PartsPage} name="home" />
<Route notfound page={NotFoundPage} />

View File

@@ -0,0 +1,34 @@
import { getActiveClasses } from 'get-active-classes'
import { TextField, FieldError } from '@redwoodjs/forms'
import { useFormContext } from 'react-hook-form'
const InputText = ({ type = 'text', className, name, validation }) => {
const { errors } = useFormContext()
return (
<>
<div className={getActiveClasses('relative inline-block', className)}>
<FieldError
className="absolute -my-4 text-sm text-red-500 font-ropa-sans"
name={name}
/>
<div
className={getActiveClasses(
'absolute inset-0 mb-2 rounded bg-gray-200 shadow-inner',
{ 'border border-red-500': errors[name] }
)}
/>
<TextField
className={getActiveClasses(
'pl-2 pt-1 text-indigo-800 font-medium mb-px pb-px bg-transparent relative w-full'
)}
name={name}
readOnly={false}
type={type}
validation={validation}
/>
</div>
</>
)
}
export default InputText

View File

@@ -0,0 +1,7 @@
import InputTextForm from './InputTextForm'
export const generated = () => {
return <InputTextForm />
}
export default { title: 'Components/InputTextForm' }

View File

@@ -0,0 +1,11 @@
import { render } from '@redwoodjs/testing'
import InputTextForm from './InputTextForm'
describe('InputTextForm', () => {
it('renders successfully', () => {
expect(() => {
render(<InputTextForm />)
}).not.toThrow()
})
})

View File

@@ -7,17 +7,18 @@ import {
} from './mockEditorParts'
import Svg from 'src/components/Svg'
import OutBound from 'src/components/OutBound'
import { useAuth } from '@redwoodjs/auth'
import ReactGA from 'react-ga'
import LoginModal from 'src/components/LoginModal'
import { useState } from 'react'
const LandingSection = () => {
const { logIn } = useAuth()
const recordedLogin = () => {
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false)
const recordedLogin = async () => {
ReactGA.event({
category: 'login',
action: 'landing section CTA',
})
logIn()
setIsLoginModalOpen(true)
}
return (
<div className="mt-16">
@@ -181,6 +182,11 @@ const LandingSection = () => {
/>
</div>
</div>
<LoginModal
open={isLoginModalOpen}
onClose={() => setIsLoginModalOpen(false)}
shouldStartWithSignup
/>
</div>
)
}

View File

@@ -0,0 +1,189 @@
import { useState } from 'react'
import Dialog from '@material-ui/core/Dialog'
import Tab from '@material-ui/core/Tab'
import Tabs from '@material-ui/core/Tabs'
import InputTextForm from 'src/components/InputTextForm'
import OutBound from 'src/components/OutBound'
import { Form, Submit } from '@redwoodjs/forms'
import { useAuth } from '@redwoodjs/auth'
import { useFlash } from '@redwoodjs/web'
import { Link, routes } from '@redwoodjs/router'
import { subscribe } from 'src/helpers/subscribe'
const LoginModal = ({ open, onClose, shouldStartWithSignup = false }) => {
const { logIn, signUp } = useAuth()
const { addMessage } = useFlash()
const [tab, setTab] = useState(shouldStartWithSignup ? 0 : 1)
const onTabChange = (_, newValue) => {
setTab(newValue)
setError('')
}
const [checkBox, setCheckBox] = useState(true)
const [error, setError] = useState('')
const onSubmitSignUp = async ({ email, password, name, userName }) => {
try {
setError('')
if (checkBox) {
subscribe({ email, addMessage })
}
await signUp({
email,
password,
remember: { full_name: name, userName },
})
onClose()
} catch (errorEvent) {
setError(errorEvent?.json?.error_description)
}
}
const onSubmitSignIn = async ({ email, password }) => {
try {
setError('')
await logIn({ email, password, remember: true })
onClose()
} catch (errorEvent) {
setError(errorEvent?.json?.error_description)
}
}
return (
<Dialog open={open} onClose={onClose}>
<div className="bg-gray-100 max-w-2xl rounded-lg shadow-lg">
<Tabs
value={tab}
onChange={onTabChange}
centered
textColor="primary"
indicatorColor="primary"
>
<Tab label="Sign Up" />
<Tab label="Sign In" />
</Tabs>
{error && (
<div className="text-sm text-red-500 font-ropa-sans pt-4 text-center">
{error}
</div>
)}
{tab === 0 ? (
<SignUpForm
onSubmitSignUp={onSubmitSignUp}
checkBox={checkBox}
setCheckBox={setCheckBox}
/>
) : (
<SignInForm onSubmitSignIn={onSubmitSignIn} />
)}
</div>
</Dialog>
)
}
const Field = ({ name, type = 'text', validation }) => (
<>
<span className="capitalize text-gray-500 text-sm align-middle my-3">
{name}:
</span>
<InputTextForm
type={type}
className="text-xl"
name={name}
validation={validation}
/>
</>
)
const HeroButton = ({ text }) => (
<Submit className="bg-texture bg-purple-800 py-6 w-full flex items-center justify-center rounded-b border border-indigo-300 border-opacity-0 hover:border-opacity-100 hover:shadow-xl">
<span className="font-bold text-2xl text-indigo-200">{text}</span>
</Submit>
)
const SignInForm = ({ onSubmitSignIn }) => (
<Form className="w-full" onSubmit={onSubmitSignIn}>
<div className="p-8">
<div
className="grid items-center gap-2"
style={{ gridTemplateColumns: 'auto 1fr' }}
>
<Field
name="email"
validation={{
required: true,
pattern: {
value: /[^@]+@[^.]+\..+/,
message: 'please enter a valid email address',
},
}}
/>
<Field
name="password"
type="password"
validation={{ required: true }}
/>
</div>
<Link
to={routes.accountRecovery()}
className="underline text-sm text-gray-500 block text-center"
>
forgot your password?
</Link>
</div>
<HeroButton text="Sign In" />
</Form>
)
const SignUpForm = ({ onSubmitSignUp, checkBox, setCheckBox }) => (
<Form className="w-full" onSubmit={onSubmitSignUp}>
<div className="p-8">
<div
className="grid items-center gap-2"
style={{ gridTemplateColumns: 'auto 1fr' }}
>
<Field name="name" validation={{ required: true }} />
<Field
name="userName"
validation={{
required: true,
pattern: {
value: /^[a-zA-Z0-9-_]+$/,
message: 'Only alphanumeric and dash characters allowed',
},
}}
/>
<Field
name="email"
validation={{
required: true,
pattern: {
value: /[^@]+@[^.]+\..+/,
message: 'please enter a valid email address',
},
}}
/>
<Field
name="password"
type="password"
validation={{ required: true }}
/>
</div>
<div className="flex pt-4">
<input
type="checkbox"
checked={checkBox}
onChange={() => setCheckBox(!checkBox)}
/>{' '}
<span className="pl-4 text-gray-500 text-sm max-w-sm">
Stay up-to-date with CadHub's progress with the founder's (
<OutBound className="underline" to="https://twitter.com/IrevDev">
Kurt's
</OutBound>
) newsletter
</span>
</div>
</div>
<HeroButton text="Sign Up" />
</Form>
)
export default LoginModal

View File

@@ -0,0 +1,7 @@
import LoginModal from './LoginModal'
export const generated = () => {
return <LoginModal open={true} />
}
export default { title: 'Components/LoginModal' }

View File

@@ -0,0 +1,11 @@
import { render } from '@redwoodjs/testing'
import LoginModal from './LoginModal'
describe('LoginModal', () => {
it('renders successfully', () => {
expect(() => {
render(<LoginModal />)
}).not.toThrow()
})
})

View File

@@ -0,0 +1,15 @@
export const subscribe = ({ email, addMessage }) => {
// subscribe to mailchimp newsletter
const path = window.location.hostname + window.location.pathname
try {
fetch(
`https://kurthutten.us10.list-manage.com/subscribe/post-json?u=cbd8888e924bdd99d06c14fa5&amp;id=6a765a8b3d&EMAIL=${email}&FNAME=Kurt&PATHNAME=${path}&c=__jp0`
)
} catch (e) {
setTimeout(() => {
addMessage('Problem subscribing to newsletter', {
classes: 'bg-red-300 text-red-900',
})
}, 1000)
}
}

View File

@@ -1,5 +1,6 @@
import { AuthProvider } from '@redwoodjs/auth'
import netlifyIdentity from 'netlify-identity-widget'
import GoTrue from 'gotrue-js'
import ReactDOM from 'react-dom'
import { RedwoodProvider, FatalErrorBoundary } from '@redwoodjs/web'
import FatalErrorPage from 'src/pages/FatalErrorPage'
@@ -16,8 +17,6 @@ import './cascade/css/main.css'
import 'monaco-editor/min/vs/editor/editor.main.css'
import './index.css'
netlifyIdentity.init()
function initCascadeStudio() {
// if ('serviceWorker' in navigator) {
// navigator.serviceWorker.register('service-worker.js').then(function(registration) {
@@ -49,9 +48,14 @@ function initCascadeStudio() {
}
initCascadeStudio()
const goTrueClient = new GoTrue({
APIUrl: 'https://cadhub.xyz/.netlify/identity',
setCookie: true,
})
ReactDOM.render(
<FatalErrorBoundary page={FatalErrorPage}>
<AuthProvider client={netlifyIdentity} type="netlify">
<AuthProvider client={goTrueClient} type="goTrue">
<RedwoodProvider>
<Routes />
</RedwoodProvider>

View File

@@ -1,12 +1,12 @@
import { useState, useEffect } from 'react'
import { Link, routes } from '@redwoodjs/router'
import { Link, routes, navigate } from '@redwoodjs/router'
import { useAuth } from '@redwoodjs/auth'
import { Flash } from '@redwoodjs/web'
import { Flash, useQuery, useFlash } from '@redwoodjs/web'
import Tooltip from '@material-ui/core/Tooltip'
import { useQuery } from '@redwoodjs/web'
import Popover from '@material-ui/core/Popover'
import { getActiveClasses } from 'get-active-classes'
import { useLocation } from '@redwoodjs/router'
import LoginModal from 'src/components/LoginModal'
import ReactGA from 'react-ga'
export const QUERY = gql`
@@ -26,11 +26,13 @@ let previousSubmission = ''
let previousUserID = ''
const MainLayout = ({ children }) => {
const { logIn, logOut, isAuthenticated, currentUser } = useAuth()
const { logOut, isAuthenticated, currentUser, client } = useAuth()
const { addMessage } = useFlash()
const { data, loading } = useQuery(QUERY, {
skip: !currentUser?.sub,
variables: { id: currentUser?.sub },
})
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false)
const [isOpen, setIsOpen] = useState(false)
const [anchorEl, setAnchorEl] = useState(null)
const [popoverId, setPopoverId] = useState(undefined)
@@ -59,7 +61,7 @@ const MainLayout = ({ children }) => {
category: 'login',
action: 'navbar login',
})
logIn()
setIsLoginModalOpen(true)
}
const { pathname, params } = useLocation()
@@ -85,6 +87,34 @@ const MainLayout = ({ children }) => {
previousUserID = currentUser
}
}, [data, currentUser, isAuthenticated])
const hash = window.location.hash
useEffect(() => {
const [key, token] = hash.slice(1).split('=')
if (key === 'confirmation_token') {
console.log('confirming with', token)
client
.confirm(token, true)
.then(() => {
addMessage('Email confirmed', { classes: 'rw-flash-success' })
})
.catch(() => {
addMessage('Problem confirming email', {
classes: 'bg-red-300 text-red-900',
})
})
} else if (key === 'recovery_token') {
client
.recover(token, true)
.then(() => {
navigate(routes.updatePassword())
})
.catch(() => {
addMessage('Problem recovering account', {
classes: 'bg-red-300 text-red-900',
})
})
}
}, [hash, client]) // complaining about not having addMessage, however adding it puts useEffect into a loop
return (
<>
<header id="cadhub-main-header">
@@ -198,7 +228,11 @@ const MainLayout = ({ children }) => {
)}
</nav>
</header>
<Flash timeout={1000} />
<Flash timeout={1500} />
<LoginModal
open={isLoginModalOpen}
onClose={() => setIsLoginModalOpen(false)}
/>
<main>{children}</main>
</>
)

View File

@@ -0,0 +1,67 @@
import { routes, navigate } from '@redwoodjs/router'
import { useAuth } from '@redwoodjs/auth'
import { Form, Submit } from '@redwoodjs/forms'
import { useFlash } from '@redwoodjs/web'
import InputTextForm from 'src/components/InputTextForm'
import MainLayout from 'src/layouts/MainLayout'
import Seo from 'src/components/Seo/Seo'
const AccountRecoveryPage = () => {
const { addMessage } = useFlash()
const { client } = useAuth()
const onSubmit = ({ email }) => {
client
.requestPasswordRecovery(email)
.then(() => {
addMessage('Email sent', { classes: 'rw-flash-success' })
setTimeout(() => {
navigate(routes.home())
}, 500)
})
.catch(() => {
addMessage('Problem sending email', {
classes: 'bg-red-300 text-red-900',
})
})
}
return (
<MainLayout>
<Seo
title="Account recovery"
description="Send recovery email"
lang="en-US"
/>
<section className="max-w-md mx-auto mt-20">
<h2 className="text-xl text-indigo-500 pb-4">Send recovery email</h2>
<Form onSubmit={onSubmit}>
<div
className="grid items-center gap-2"
style={{ gridTemplateColumns: 'auto 1fr' }}
>
<span className="capitalize text-gray-500 text-sm align-middle my-3">
email:
</span>
<InputTextForm
className="text-xl"
name="email"
validation={{
required: true,
pattern: {
value: /[^@]+@[^.]+\..+/,
message: 'please enter a valid email address',
},
}}
/>
</div>
<Submit className="bg-indigo-200 text-indigo-800 p-2 px-4 shadow hover:shadow-lg mt-4 rounded">
Send email
</Submit>
</Form>
</section>
</MainLayout>
)
}
export default AccountRecoveryPage

View File

@@ -0,0 +1,7 @@
import AccountRecoveryPage from './AccountRecoveryPage'
export const generated = () => {
return <AccountRecoveryPage />
}
export default { title: 'Pages/AccountRecoveryPage' }

View File

@@ -0,0 +1,11 @@
import { render } from '@redwoodjs/testing'
import AccountRecoveryPage from './AccountRecoveryPage'
describe('AccountRecoveryPage', () => {
it('renders successfully', () => {
expect(() => {
render(<AccountRecoveryPage />)
}).not.toThrow()
})
})

View File

@@ -0,0 +1,78 @@
import { routes, navigate } from '@redwoodjs/router'
import { useAuth } from '@redwoodjs/auth'
import { Form, Submit } from '@redwoodjs/forms'
import { useFlash } from '@redwoodjs/web'
import InputTextForm from 'src/components/InputTextForm'
import MainLayout from 'src/layouts/MainLayout'
import Seo from 'src/components/Seo/Seo'
const UpdatePasswordPage = () => {
const { addMessage } = useFlash()
const { client } = useAuth()
const onSubmit = ({ password, confirm }) => {
if (password !== confirm || !password) {
addMessage("Passwords don't match, try again", {
classes: 'bg-red-300 text-red-900',
})
return
}
client
.currentUser()
.update({ password })
.then(() => {
addMessage('Password updated', { classes: 'rw-flash-success' })
setTimeout(() => {
navigate(routes.home())
}, 500)
})
.catch(() => {
addMessage('Problem updating password', {
classes: 'bg-red-300 text-red-900',
})
})
}
return (
<MainLayout>
<Seo title="Update Password" description="Update Password" lang="en-US" />
<section className="max-w-md mx-auto mt-20">
<h2 className="text-xl text-indigo-500 pb-4">Reset Password</h2>
<Form onSubmit={onSubmit}>
<div
className="grid items-center gap-2"
style={{ gridTemplateColumns: 'auto 1fr' }}
>
<span className="capitalize text-gray-500 text-sm align-middle my-3">
password:
</span>
<InputTextForm
className="text-xl"
name="password"
type="password"
validation={{
required: true,
}}
/>
<span className="capitalize text-gray-500 text-sm align-middle my-3">
confirm:
</span>
<InputTextForm
className="text-xl"
name="confirm"
type="password"
validation={{
required: true,
}}
/>
</div>
<Submit className="bg-indigo-200 text-indigo-800 p-2 px-4 shadow hover:shadow-lg mt-4 rounded">
Update
</Submit>
</Form>
</section>
</MainLayout>
)
}
export default UpdatePasswordPage

View File

@@ -0,0 +1,7 @@
import UpdatePasswordPage from './UpdatePasswordPage'
export const generated = () => {
return <UpdatePasswordPage />
}
export default { title: 'Pages/UpdatePasswordPage' }

View File

@@ -0,0 +1,11 @@
import { render } from '@redwoodjs/testing'
import UpdatePasswordPage from './UpdatePasswordPage'
describe('UpdatePasswordPage', () => {
it('renders successfully', () => {
expect(() => {
render(<UpdatePasswordPage />)
}).not.toThrow()
})
})

View File

@@ -2366,10 +2366,10 @@
lodash.omitby "^4.6.0"
merge-graphql-schemas "^1.7.6"
"@redwoodjs/auth@^0.20.0":
version "0.20.0"
resolved "https://registry.yarnpkg.com/@redwoodjs/auth/-/auth-0.20.0.tgz#c08c0b735a0b09ef84dc6d357fa2803d61f1d389"
integrity sha512-M1rPtiXzU7YagQ120zEkp5qvJruWRcFLUfBhgexGYgnEkqFIKFGLQL9pBywoZ6kxNjI7x/pbghz6FS7H/lzw/g==
"@redwoodjs/auth@^0.21.0":
version "0.21.0"
resolved "https://registry.yarnpkg.com/@redwoodjs/auth/-/auth-0.21.0.tgz#967deef0d0421ea9f6bc205857ad9265d2f5df55"
integrity sha512-o3HuRTs79BqmnZX2zvK6+ffebxJE+T/nqwDDdOmCjXUitSsYbLSJkG4ffUuMtSWCFCxf/ytaFB245nS8Vin3XQ==
"@redwoodjs/cli@^0.20.0":
version "0.20.0"
@@ -8671,6 +8671,13 @@ good-listener@^1.2.2:
dependencies:
delegate "^3.1.2"
gotrue-js@^0.9.27:
version "0.9.27"
resolved "https://registry.yarnpkg.com/gotrue-js/-/gotrue-js-0.9.27.tgz#ed01b47e97781f10f26458ff77d83d97bcc65e08"
integrity sha512-XSQ9XGELrnf6bKYaGk+2z1E6aW6+wZ3S6ns3JQz9tXesBuwVPDv/xOTTZ/Qrtu85GJgxTjhJH447w09a73Tneg==
dependencies:
micro-api-client "^3.2.1"
graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4:
version "4.2.4"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
@@ -11331,6 +11338,11 @@ methods@~1.1.2:
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
micro-api-client@^3.2.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/micro-api-client/-/micro-api-client-3.3.0.tgz#52dd567d322f10faffe63d19d4feeac4e4ffd215"
integrity sha512-y0y6CUB9RLVsy3kfgayU28746QrNMpSm9O/AYGNsBgOkJr/X/Jk0VLGoO8Ude7Bpa8adywzF+MzXNZRFRsNPhg==
microevent.ts@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0"