-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Migrate signup to new lightweight login modal
- Loading branch information
Showing
11 changed files
with
407 additions
and
633 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,335 @@ | ||
"use client"; | ||
|
||
import React, { useState } from 'react'; | ||
import styled, { keyframes } from 'styled-components'; | ||
|
||
import { asErrorLike } from '@httptoolkit/util'; | ||
import { sendAuthCode, loginWithCode } from '@httptoolkit/accounts'; | ||
|
||
import { accountStore } from '@/lib/store/account-store'; | ||
import { observer } from 'mobx-react-lite'; | ||
import { Heading } from '@/components/elements/heading'; | ||
import { Logo, X, CaretLeft } from '@/components/elements/icon'; | ||
import { Button } from '@/components/elements/button'; | ||
import { Link } from '@/components/elements/link'; | ||
|
||
const Modal = styled.dialog` | ||
position: fixed; | ||
top: 50%; | ||
left: 50%; | ||
transform: translate(-50%, -50%); | ||
z-index: 100; | ||
margin: 0; | ||
width: 90%; | ||
@media (min-width: ${({ theme }) => theme.screens.lg}) { | ||
width: auto; | ||
max-width: 340px; | ||
} | ||
background: white; | ||
color: black; | ||
border-radius: 16px; | ||
padding: 0; | ||
box-shadow: 0 0 0 1px var(--button-border) inset; | ||
outline: none; | ||
border: none; | ||
background-color: var(--dark-grey); | ||
&::backdrop { | ||
opacity: 0.9; | ||
background: radial-gradient(circle, var(--medium-grey), var(--light-grey)); | ||
} | ||
`; | ||
|
||
const CtaButton = styled(Button)` | ||
margin: 20px; | ||
width: calc(100% - 40px); | ||
box-sizing: border-box; | ||
`; | ||
|
||
const CloseDialogButton = styled.button` | ||
position: absolute; | ||
top: 0; | ||
right: 0; | ||
padding: 16px; | ||
background: none; | ||
border: none; | ||
color: var(--light-grey); | ||
cursor: pointer; | ||
&:hover { | ||
color: var(--white); | ||
} | ||
`; | ||
|
||
const BackButton = styled.button` | ||
position: absolute; | ||
top: 0; | ||
left: 0; | ||
padding: 16px; | ||
background: none; | ||
border: none; | ||
color: var(--light-grey); | ||
cursor: pointer; | ||
&:hover { | ||
color: var(--white); | ||
} | ||
`; | ||
|
||
const Form = styled.form` | ||
display: flex; | ||
flex-direction: column; | ||
align-items: center; | ||
justify-content: center; | ||
` | ||
|
||
const HeadingLogo = styled(Logo)` | ||
margin: 48px 16px 16px; | ||
width: 30%; | ||
fill: var(--cinnabar-red); | ||
`; | ||
|
||
const Title = styled(Heading)` | ||
margin: 16px 32px; | ||
text-align: center; | ||
`; | ||
|
||
const Subtitle = styled(Heading)` | ||
margin: -16px 32px 16px; | ||
text-align: center; | ||
`; | ||
|
||
const Email = styled.span` | ||
white-space: break-spaces; | ||
word-break: break-word; | ||
hyphens: auto; | ||
`; | ||
|
||
const Input = styled.input` | ||
padding: 16px; | ||
margin: 16px 0 0; | ||
width: 100%; | ||
border-style: solid; | ||
border-color: var(--medium-grey); | ||
background-color: var(--ink-black); | ||
border-width: 1px 0 1px 0; | ||
z-index: 1; | ||
font-size: ${({ theme }) => theme.fontSizes.text.m}; | ||
&:focus { | ||
border-color: var(--white); | ||
} | ||
`; | ||
|
||
const SmallPrint = styled.p` | ||
margin: 0; | ||
padding: 10px 16px 12px; | ||
width: 100%; | ||
font-size: ${({ theme }) => theme.fontSizes.text.s}; | ||
font-style: italic; | ||
background-color: var(--darkish-grey); | ||
color: var(--light-grey); | ||
`; | ||
|
||
const spin = keyframes` | ||
0% { transform: rotate(0deg); } | ||
100% { transform: rotate(360deg); } | ||
`; | ||
|
||
const Spinner = styled.div` | ||
border: 4px solid rgba(0, 0, 0, 0.1); | ||
border-top: 4px solid #007bff; | ||
border-radius: 50%; | ||
width: 24px; | ||
height: 24px; | ||
animation: ${spin} 1s linear infinite; | ||
margin: 10px 0; | ||
`; | ||
|
||
const ErrorMessage = styled.div` | ||
color: red; | ||
margin: 16px 20px 0; | ||
`; | ||
|
||
export const LoginModal = observer(() => { | ||
const handleDialogClose = React.useCallback(() => { | ||
accountStore.endLogin(); | ||
}, []); | ||
|
||
if (!accountStore.loginModalVisible) return null; | ||
|
||
return <Modal | ||
ref={(dialog) => dialog?.showModal()} | ||
onClose={handleDialogClose} | ||
> | ||
<CloseDialogButton | ||
onClick={handleDialogClose} | ||
aria-label="Close dialog" | ||
> | ||
<X size="24" /> | ||
</CloseDialogButton> | ||
<LoginFields /> | ||
</Modal>; | ||
}); | ||
|
||
const focusInput = (input: HTMLInputElement | null) => { | ||
requestAnimationFrame(() => | ||
input?.focus() | ||
); | ||
} | ||
|
||
const LoginFields = () => { | ||
const [email, setEmail] = useState(''); | ||
const [code, setCode] = useState(''); | ||
|
||
const [isEmailSent, setIsEmailSent] = useState(false); | ||
const [isLoading, setIsLoading] = useState(false); | ||
const [error, setError] = useState<string | false>(false); | ||
|
||
const handleEmailSubmit = async (e: React.FormEvent) => { | ||
e.preventDefault(); | ||
|
||
setIsLoading(true); | ||
setError(false); | ||
|
||
try { | ||
await sendAuthCode(email, 'website'); | ||
setIsLoading(false); | ||
setIsEmailSent(true); | ||
} catch (e) { | ||
setIsLoading(false); | ||
setError(asErrorLike(e).message || 'An error occurred'); | ||
} | ||
}; | ||
|
||
const handleBackButton = () => { | ||
setIsEmailSent(false); | ||
setError(false); | ||
}; | ||
|
||
const handleCodeSubmit = async (e: React.FormEvent) => { | ||
e.preventDefault(); | ||
|
||
setIsLoading(true); | ||
setError(false); | ||
|
||
try { | ||
await loginWithCode(email, code); | ||
await accountStore.finalizeLogin(); | ||
// We never unset isLoading - the modal disappears entirely when the | ||
// account store state is fully updated, and we want to spin till then. | ||
} catch (e) { | ||
setIsLoading(false); | ||
setError(asErrorLike(e).message || 'An error occurred'); | ||
} | ||
}; | ||
|
||
return !isEmailSent | ||
? <Form onSubmit={handleEmailSubmit}> | ||
<HeadingLogo /> | ||
<Title fontSize='m'> | ||
Enter your email | ||
</Title> | ||
<Input | ||
name="email" | ||
type="email" | ||
required | ||
placeholder="you@email.example" | ||
ref={focusInput} | ||
value={email} | ||
onChange={(e) => setEmail(e.target.value)} | ||
disabled={isLoading} | ||
/> | ||
|
||
{error && | ||
<ErrorMessage>{error}</ErrorMessage> | ||
} | ||
|
||
<CtaButton type="submit" disabled={isLoading}> | ||
{isLoading ? <Spinner /> : 'Send Code'} | ||
</CtaButton> | ||
|
||
<SmallPrint> | ||
By creating an account you accept the <Link | ||
href="/terms-of-service" | ||
target="_blank" | ||
> | ||
Terms of Service | ||
</Link> & <Link | ||
href="/privacy-policy" | ||
target="_blank" | ||
> | ||
Privacy Policy | ||
</Link>. | ||
</SmallPrint> | ||
</Form> | ||
: | ||
<Form onSubmit={handleCodeSubmit}> | ||
<BackButton | ||
type="button" | ||
onClick={handleBackButton} | ||
aria-label="Go back" | ||
> | ||
<CaretLeft size='24' /> | ||
</BackButton> | ||
<HeadingLogo /> | ||
<Title fontSize='m'> | ||
Enter the code | ||
</Title> | ||
<Subtitle fontSize='xs'> | ||
sent to you at<br/><Email> | ||
{ email } | ||
</Email> | ||
</Subtitle> | ||
<Input | ||
name="otp" | ||
type="text" | ||
inputMode="numeric" | ||
pattern="\d{6}" | ||
required | ||
placeholder="Enter the 6 digit code" | ||
ref={focusInput} | ||
value={code} | ||
onChange={(e) => { | ||
const input = e.target.value; | ||
const numberInput = input.replace(/\D/g, '').slice(0, 6); | ||
setCode(numberInput); | ||
}} | ||
disabled={isLoading} | ||
/> | ||
|
||
{error && | ||
<ErrorMessage>{error}</ErrorMessage> | ||
} | ||
|
||
<CtaButton type="submit" disabled={isLoading}> | ||
{isLoading ? <Spinner /> : 'Login'} | ||
</CtaButton> | ||
|
||
<SmallPrint> | ||
By creating an account you accept the <Link | ||
href="/terms-of-service" | ||
target="_blank" | ||
> | ||
Terms of Service | ||
</Link> & <Link | ||
href="/privacy-policy" | ||
target="_blank" | ||
> | ||
Privacy Policy | ||
</Link>. | ||
</SmallPrint> | ||
</Form> | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
8 changes: 5 additions & 3 deletions
8
src/components/sections/pricing/plans/components/login-info/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1
src/components/sections/pricing/plans/components/login-info/login-info.types.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
export interface LoginInfoProps { | ||
email?: string; | ||
logOut: () => void; | ||
isLoggedIn: boolean; | ||
} |
Oops, something went wrong.