Skip to content

Commit da2c2d9

Browse files
committed
Migrate signup to new lightweight login modal
1 parent 6687e1c commit da2c2d9

File tree

11 files changed

+407
-633
lines changed

11 files changed

+407
-633
lines changed

package-lock.json

Lines changed: 21 additions & 581 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"homepage": "https://github.com/httptoolkit/httptoolkit-website",
2727
"dependencies": {
2828
"@docsearch/react": "^3.6.0",
29-
"@httptoolkit/accounts": "^2.2.0",
29+
"@httptoolkit/accounts": "^3.0.0",
30+
"@httptoolkit/util": "^0.1.5",
3031
"@phosphor-icons/react": "^2.1.4",
3132
"@radix-ui/react-accordion": "^1.1.2",
3233
"@radix-ui/react-dropdown-menu": "^2.0.6",

src/app/(pricing)/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import { CaretRight } from '@/components/elements/icon';
88
import { Layout } from '@/components/layout';
99
import { PricingComparison } from '@/components/sections/pricing/comparison';
1010
import { TextWithAccordion } from '@/components/sections/text-with-accordion';
11+
import { LoginModal } from '@/components/modules/login-modal';
1112

1213
export default function PricingLayout({ children }: { children: React.ReactNode }) {
1314
return (
1415
<Layout>
16+
<LoginModal />
1517
<Suspense>{children}</Suspense>
1618
<PricingComparison
1719
title="Features"
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
"use client";
2+
3+
import React, { useState } from 'react';
4+
import styled, { keyframes } from 'styled-components';
5+
6+
import { asErrorLike } from '@httptoolkit/util';
7+
import { sendAuthCode, loginWithCode } from '@httptoolkit/accounts';
8+
9+
import { accountStore } from '@/lib/store/account-store';
10+
import { observer } from 'mobx-react-lite';
11+
import { Heading } from '@/components/elements/heading';
12+
import { Logo, X, CaretLeft } from '@/components/elements/icon';
13+
import { Button } from '@/components/elements/button';
14+
import { Link } from '@/components/elements/link';
15+
16+
const Modal = styled.dialog`
17+
position: fixed;
18+
top: 50%;
19+
left: 50%;
20+
transform: translate(-50%, -50%);
21+
z-index: 100;
22+
margin: 0;
23+
24+
width: 90%;
25+
@media (min-width: ${({ theme }) => theme.screens.lg}) {
26+
width: auto;
27+
max-width: 340px;
28+
}
29+
30+
background: white;
31+
color: black;
32+
33+
border-radius: 16px;
34+
padding: 0;
35+
box-shadow: 0 0 0 1px var(--button-border) inset;
36+
37+
outline: none;
38+
border: none;
39+
40+
background-color: var(--dark-grey);
41+
42+
&::backdrop {
43+
opacity: 0.9;
44+
background: radial-gradient(circle, var(--medium-grey), var(--light-grey));
45+
}
46+
`;
47+
48+
const CtaButton = styled(Button)`
49+
margin: 20px;
50+
width: calc(100% - 40px);
51+
box-sizing: border-box;
52+
`;
53+
54+
const CloseDialogButton = styled.button`
55+
position: absolute;
56+
top: 0;
57+
right: 0;
58+
padding: 16px;
59+
60+
background: none;
61+
border: none;
62+
color: var(--light-grey);
63+
cursor: pointer;
64+
65+
&:hover {
66+
color: var(--white);
67+
}
68+
`;
69+
70+
const BackButton = styled.button`
71+
position: absolute;
72+
top: 0;
73+
left: 0;
74+
padding: 16px;
75+
76+
background: none;
77+
border: none;
78+
color: var(--light-grey);
79+
cursor: pointer;
80+
81+
&:hover {
82+
color: var(--white);
83+
}
84+
`;
85+
86+
const Form = styled.form`
87+
display: flex;
88+
flex-direction: column;
89+
align-items: center;
90+
justify-content: center;
91+
`
92+
93+
const HeadingLogo = styled(Logo)`
94+
margin: 48px 16px 16px;
95+
width: 30%;
96+
fill: var(--cinnabar-red);
97+
`;
98+
99+
const Title = styled(Heading)`
100+
margin: 16px 32px;
101+
text-align: center;
102+
`;
103+
104+
const Subtitle = styled(Heading)`
105+
margin: -16px 32px 16px;
106+
text-align: center;
107+
`;
108+
109+
const Email = styled.span`
110+
white-space: break-spaces;
111+
word-break: break-word;
112+
hyphens: auto;
113+
`;
114+
115+
const Input = styled.input`
116+
padding: 16px;
117+
margin: 16px 0 0;
118+
width: 100%;
119+
120+
border-style: solid;
121+
border-color: var(--medium-grey);
122+
background-color: var(--ink-black);
123+
124+
border-width: 1px 0 1px 0;
125+
z-index: 1;
126+
127+
font-size: ${({ theme }) => theme.fontSizes.text.m};
128+
129+
&:focus {
130+
border-color: var(--white);
131+
}
132+
`;
133+
134+
const SmallPrint = styled.p`
135+
margin: 0;
136+
padding: 10px 16px 12px;
137+
width: 100%;
138+
139+
font-size: ${({ theme }) => theme.fontSizes.text.s};
140+
font-style: italic;
141+
142+
background-color: var(--darkish-grey);
143+
color: var(--light-grey);
144+
`;
145+
146+
const spin = keyframes`
147+
0% { transform: rotate(0deg); }
148+
100% { transform: rotate(360deg); }
149+
`;
150+
151+
const Spinner = styled.div`
152+
border: 4px solid rgba(0, 0, 0, 0.1);
153+
border-top: 4px solid #007bff;
154+
border-radius: 50%;
155+
width: 24px;
156+
height: 24px;
157+
animation: ${spin} 1s linear infinite;
158+
margin: 10px 0;
159+
`;
160+
161+
const ErrorMessage = styled.div`
162+
color: red;
163+
margin: 16px 20px 0;
164+
`;
165+
166+
export const LoginModal = observer(() => {
167+
const handleDialogClose = React.useCallback(() => {
168+
accountStore.endLogin();
169+
}, []);
170+
171+
if (!accountStore.loginModalVisible) return null;
172+
173+
return <Modal
174+
ref={(dialog) => dialog?.showModal()}
175+
onClose={handleDialogClose}
176+
>
177+
<CloseDialogButton
178+
onClick={handleDialogClose}
179+
aria-label="Close dialog"
180+
>
181+
<X size="24" />
182+
</CloseDialogButton>
183+
<LoginFields />
184+
</Modal>;
185+
});
186+
187+
const focusInput = (input: HTMLInputElement | null) => {
188+
requestAnimationFrame(() =>
189+
input?.focus()
190+
);
191+
}
192+
193+
const LoginFields = () => {
194+
const [email, setEmail] = useState('');
195+
const [code, setCode] = useState('');
196+
197+
const [isEmailSent, setIsEmailSent] = useState(false);
198+
const [isLoading, setIsLoading] = useState(false);
199+
const [error, setError] = useState<string | false>(false);
200+
201+
const handleEmailSubmit = async (e: React.FormEvent) => {
202+
e.preventDefault();
203+
204+
setIsLoading(true);
205+
setError(false);
206+
207+
try {
208+
await sendAuthCode(email, 'website');
209+
setIsLoading(false);
210+
setIsEmailSent(true);
211+
} catch (e) {
212+
setIsLoading(false);
213+
setError(asErrorLike(e).message || 'An error occurred');
214+
}
215+
};
216+
217+
const handleBackButton = () => {
218+
setIsEmailSent(false);
219+
setError(false);
220+
};
221+
222+
const handleCodeSubmit = async (e: React.FormEvent) => {
223+
e.preventDefault();
224+
225+
setIsLoading(true);
226+
setError(false);
227+
228+
try {
229+
await loginWithCode(email, code);
230+
await accountStore.finalizeLogin();
231+
// We never unset isLoading - the modal disappears entirely when the
232+
// account store state is fully updated, and we want to spin till then.
233+
} catch (e) {
234+
setIsLoading(false);
235+
setError(asErrorLike(e).message || 'An error occurred');
236+
}
237+
};
238+
239+
return !isEmailSent
240+
? <Form onSubmit={handleEmailSubmit}>
241+
<HeadingLogo />
242+
<Title fontSize='m'>
243+
Enter your email
244+
</Title>
245+
<Input
246+
name="email"
247+
type="email"
248+
required
249+
placeholder="[email protected]"
250+
ref={focusInput}
251+
value={email}
252+
onChange={(e) => setEmail(e.target.value)}
253+
disabled={isLoading}
254+
/>
255+
256+
{error &&
257+
<ErrorMessage>{error}</ErrorMessage>
258+
}
259+
260+
<CtaButton type="submit" disabled={isLoading}>
261+
{isLoading ? <Spinner /> : 'Send Code'}
262+
</CtaButton>
263+
264+
<SmallPrint>
265+
By creating an account you accept the <Link
266+
href="/terms-of-service"
267+
target="_blank"
268+
>
269+
Terms of Service
270+
</Link> & <Link
271+
href="/privacy-policy"
272+
target="_blank"
273+
>
274+
Privacy Policy
275+
</Link>.
276+
</SmallPrint>
277+
</Form>
278+
:
279+
<Form onSubmit={handleCodeSubmit}>
280+
<BackButton
281+
type="button"
282+
onClick={handleBackButton}
283+
aria-label="Go back"
284+
>
285+
<CaretLeft size='24' />
286+
</BackButton>
287+
<HeadingLogo />
288+
<Title fontSize='m'>
289+
Enter the code
290+
</Title>
291+
<Subtitle fontSize='xs'>
292+
sent to you at<br/><Email>
293+
{ email }
294+
</Email>
295+
</Subtitle>
296+
<Input
297+
name="otp"
298+
type="text"
299+
inputMode="numeric"
300+
pattern="\d{6}"
301+
required
302+
placeholder="Enter the 6 digit code"
303+
ref={focusInput}
304+
value={code}
305+
onChange={(e) => {
306+
const input = e.target.value;
307+
const numberInput = input.replace(/\D/g, '').slice(0, 6);
308+
setCode(numberInput);
309+
}}
310+
disabled={isLoading}
311+
/>
312+
313+
{error &&
314+
<ErrorMessage>{error}</ErrorMessage>
315+
}
316+
317+
<CtaButton type="submit" disabled={isLoading}>
318+
{isLoading ? <Spinner /> : 'Login'}
319+
</CtaButton>
320+
321+
<SmallPrint>
322+
By creating an account you accept the <Link
323+
href="/terms-of-service"
324+
target="_blank"
325+
>
326+
Terms of Service
327+
</Link> & <Link
328+
href="/privacy-policy"
329+
target="_blank"
330+
>
331+
Privacy Policy
332+
</Link>.
333+
</SmallPrint>
334+
</Form>
335+
};

src/components/sections/pricing/comparison/components/heading-plan/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const HeadingPlan = observer(({ id, title, downloadButton }: HeadingPlanP
1717
<Heading as="h3" fontSize="xs" color="lightGrey" textAlign="center">
1818
{title}
1919
</Heading>
20-
{getPlanCTA(id, accountStore, accountStore.waitingForPurchase, 'monthly')}
20+
{getPlanCTA(id, accountStore, 'monthly')}
2121
</StyledHeadingPlanWrapper>
2222
);
2323
});

src/components/sections/pricing/plans/components/login-info/index.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
'use client';
22

3-
import { logOut } from '@httptoolkit/accounts';
4-
53
import { StyledLoginInfoWrapper } from './login-info.styles';
64
import type { LoginInfoProps } from './login-info.types';
75

86
import { Button } from '@/components/elements/button';
97
import { Link } from '@/components/elements/link';
108
import { Text } from '@/components/elements/text';
119

12-
export const LoginInfo = ({ isLoggedIn, email }: LoginInfoProps) => {
10+
export const LoginInfo = ({
11+
isLoggedIn,
12+
logOut,
13+
email
14+
}: LoginInfoProps) => {
1315
if (!isLoggedIn) {
1416
return (
1517
<Text fontSize="m" textAlign="center" color="darkGrey">
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export interface LoginInfoProps {
22
email?: string;
3+
logOut: () => void;
34
isLoggedIn: boolean;
45
}

0 commit comments

Comments
 (0)