Skip to content

Commit 5372a4c

Browse files
authored
feat: move auth from modal to dedicated page (#458)
1 parent ca3298f commit 5372a4c

20 files changed

Lines changed: 826 additions & 451 deletions

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- uses: actions/checkout@v7.0.0
1717
- uses: actions/setup-node@v6.4.0
1818
with:
19-
node-version: 24.17.0
19+
node-version: 24.18.0
2020

2121
- run: yarn install
2222

.github/workflows/code-health.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ on:
66
- main
77

88
env:
9-
NODE_VERSION: 24.17.0
9+
NODE_VERSION: 24.18.0
1010

1111
jobs:
1212
setup:

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
24.17.0
1+
24.18.0

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ Supabase storage buckets:
156156

157157
### Environment Requirements
158158

159-
- Node.js 24.17.0
159+
- Node.js 24.18.0
160160
- Docker (for local Supabase)
161161
- Yarn is used as a package manager and is automatically available via Corepack (bundled with Node.js)
162162

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ There's also a public roadmap on Featurebase where you can upvote and suggest ne
8888
### Prerequisites
8989

9090
- [Git](https://git-scm.com/)
91-
- [Node.js](https://nodejs.org/en/) (24.17.0)
91+
- [Node.js](https://nodejs.org/en/) (24.18.0)
9292
- [Docker](https://docs.docker.com/get-started/get-docker/)
9393
- Yarn is used as a package manager and is automatically available via Corepack (bundled with Node.js)
9494

docs/CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ By participating in this project, you agree to maintain a respectful and inclusi
1111
### Prerequisites
1212

1313
- [Git](https://git-scm.com/)
14-
- [Node.js](https://nodejs.org/en/) (24.17.0)
14+
- [Node.js](https://nodejs.org/en/) (24.18.0)
1515
- [Docker](https://docs.docker.com/get-started/get-docker/)
1616
- Yarn is used as a package manager and is automatically available via Corepack (bundled with Node.js)
1717

package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
"react-swipeable": "^7.0.2",
7979
"rollbar": "3.1.0",
8080
"type-fest": "^5.7.0",
81-
"typescript-eslint": "^8.61.1",
81+
"typescript-eslint": "^8.62.0",
8282
"zustand": "^5.0.14"
8383
},
8484
"devDependencies": {
@@ -100,27 +100,27 @@
100100
"@types/prop-types": "^15.7.15",
101101
"@types/react": "^19.2.17",
102102
"@types/react-dom": "^19.2.3",
103-
"@vitejs/plugin-react": "^6.0.2",
103+
"@vitejs/plugin-react": "^6.0.3",
104104
"@vitest/coverage-v8": "4.1.9",
105105
"eslint": "^10.5.0",
106106
"eslint-config-prettier": "^10.1.8",
107107
"eslint-plugin-perfectionist": "^5.9.1",
108108
"eslint-plugin-react-hooks": "^7.1.1",
109109
"eslint-plugin-switch-case": "^4.0.0",
110110
"eslint-plugin-unused-imports": "^4.4.1",
111-
"globals": "^17.6.0",
111+
"globals": "^17.7.0",
112112
"husky": "^9.1.7",
113113
"jsdom": "^29.1.1",
114-
"lint-staged": "^17.0.7",
115-
"prettier": "^3.8.4",
114+
"lint-staged": "^17.0.8",
115+
"prettier": "^3.8.5",
116116
"prettier-plugin-tailwindcss": "^0.8.0",
117117
"rollup-plugin-visualizer": "^7.0.1",
118118
"rollup-plugin-webpack-stats": "^3.1.3",
119-
"supabase": "^2.107.0",
119+
"supabase": "^2.108.0",
120120
"tailwindcss": "^4.3.1",
121121
"typescript": "^6.0.3",
122122
"user-agent-data-types": "^0.4.3",
123-
"vite": "^8.0.16",
123+
"vite": "^8.1.0",
124124
"vite-plugin-rollbar-sourcemap": "^0.0.12",
125125
"vitest": "^4.1.9"
126126
},

src/Routes.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ import React from 'react';
22
import { Route, Routes, Navigate } from 'react-router';
33

44
import {
5+
LoginPage,
56
NotesPage,
67
HabitsPage,
78
AccountPage,
9+
RegisterPage,
810
DayCalendarPage,
911
OAuthConsentPage,
1012
HabitDetailsPage,
1113
WeekCalendarPage,
1214
MonthCalendarPage,
15+
ResetPasswordPage,
1316
} from '@pages';
1417

1518
const AppRoutes = () => {
@@ -31,6 +34,9 @@ const AppRoutes = () => {
3134
<Route path="/habits/:habitId" element={<HabitDetailsPage />} />
3235
<Route path="/notes" element={<NotesPage />} />
3336
<Route path="/account" element={<AccountPage />} />
37+
<Route path="/login" element={<LoginPage />} />
38+
<Route path="/register" element={<RegisterPage />} />
39+
<Route path="/reset-password" element={<ResetPasswordPage />} />
3440
<Route path="/oauth/consent" element={<OAuthConsentPage />} />
3541
<Route path="*" element={<Navigate replace to="/calendar/month" />} />
3642
</Routes>

src/components/account/AuthForm.test.tsx

Lines changed: 10 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,16 @@ import { act, render, waitFor, fireEvent } from '@testing-library/react';
22
import React from 'react';
33
import { it, vi, expect, describe } from 'vitest';
44

5-
import { noop } from '@utils';
6-
75
import AuthForm from './AuthForm';
86

97
describe(AuthForm.name, () => {
108
it('should call onSubmit with email and password', async () => {
119
const onSubmit = vi.fn();
12-
const onCancel = vi.fn();
13-
const disabled = false;
14-
const submitButtonLabel = 'Submit';
1510
const { getByPlaceholderText, getByTestId } = render(
1611
<AuthForm
17-
mode="login"
1812
onSubmit={onSubmit}
19-
onCancel={onCancel}
20-
onModeChange={noop}
21-
goBackToLogin={noop}
22-
isAuthenticating={disabled}
23-
submitButtonLabel={submitButtonLabel}
13+
submitLabel="Submit"
14+
isAuthenticating={false}
2415
/>
2516
);
2617

@@ -35,35 +26,20 @@ describe(AuthForm.name, () => {
3526
});
3627

3728
await waitFor(() => {
38-
expect(onSubmit).toHaveBeenCalledWith('email', 'password', '');
29+
expect(onSubmit).toHaveBeenCalledWith('email', 'password');
3930
});
4031
});
4132

42-
it('should call onCancel when cancel button is clicked', async () => {
43-
const onSubmit = vi.fn();
44-
const onCancel = vi.fn();
45-
const disabled = false;
46-
const submitButtonLabel = 'Submit';
47-
const { getByText } = render(
33+
it('should not render a password field when withPassword is false', () => {
34+
const { queryByPlaceholderText } = render(
4835
<AuthForm
49-
mode="login"
50-
onSubmit={onSubmit}
51-
onCancel={onCancel}
52-
onModeChange={noop}
53-
goBackToLogin={noop}
54-
isAuthenticating={disabled}
55-
submitButtonLabel={submitButtonLabel}
36+
onSubmit={vi.fn()}
37+
withPassword={false}
38+
submitLabel="Submit"
39+
isAuthenticating={false}
5640
/>
5741
);
5842

59-
const cancelButton = getByText('Cancel');
60-
61-
act(() => {
62-
fireEvent.click(cancelButton);
63-
});
64-
65-
await waitFor(() => {
66-
expect(onCancel).toHaveBeenCalled();
67-
});
43+
expect(queryByPlaceholderText('Password')).toBeNull();
6844
});
6945
});

src/components/account/AuthForm.tsx

Lines changed: 49 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,134 +1,81 @@
1-
import { cn, Input, Label, TextField } from '@heroui/react';
2-
import { type SubmitEventHandler } from 'react';
1+
import { Input, Label, TextField } from '@heroui/react';
2+
import { type ReactNode, type SubmitEventHandler } from 'react';
33

44
import { CustomButton } from '@components';
55
import { useTextField } from '@hooks';
66

77
import PasswordInput from './PasswordInput';
88

99
type AuthFormProps = {
10+
canReset?: boolean;
11+
description?: ReactNode;
12+
footer?: ReactNode;
1013
isAuthenticating: boolean;
11-
mode: 'login' | 'register' | 'reset-password';
12-
submitButtonLabel: string;
13-
goBackToLogin: () => void;
14-
onCancel: () => void;
15-
onModeChange: (mode: 'login' | 'register' | 'reset-password') => void;
16-
onSubmit: (email: string, password: string, name: string) => void;
14+
passwordAutoComplete?: string;
15+
submitLabel: string;
16+
withPassword?: boolean;
17+
onSubmit: (email: string, password: string) => void;
1718
};
1819

1920
const AuthForm = ({
20-
goBackToLogin,
21+
canReset,
22+
description,
23+
footer,
2124
isAuthenticating,
22-
mode,
23-
onCancel,
24-
onModeChange,
2525
onSubmit,
26-
submitButtonLabel,
26+
passwordAutoComplete,
27+
submitLabel,
28+
withPassword = true,
2729
}: AuthFormProps) => {
28-
const [email, handleEmailChange, clearEmail] = useTextField();
29-
const [name, handleNameChange, clearName] = useTextField();
30-
const [password, handlePasswordChange, clearPassword] = useTextField();
30+
const [email, handleEmailChange] = useTextField();
31+
const [password, handlePasswordChange] = useTextField();
3132

32-
const handleSubmit: SubmitEventHandler = async (event) => {
33+
const handleSubmit: SubmitEventHandler = (event) => {
3334
event.preventDefault();
34-
onSubmit(email, password, name);
35-
};
36-
37-
const clearValues = () => {
38-
clearEmail();
39-
clearPassword();
40-
clearName();
41-
};
42-
43-
const handleCancel = () => {
44-
clearValues();
45-
onCancel();
35+
onSubmit(email, password);
4636
};
4737

4838
return (
49-
<form
50-
onSubmit={handleSubmit}
51-
data-testid="submit-form"
52-
className={cn(mode === 'reset-password' && 'py-3')}
53-
>
39+
<form className="w-full" onSubmit={handleSubmit} data-testid="submit-form">
5440
<div className="flex flex-col gap-4">
55-
{mode === 'reset-password' && (
56-
<p>
57-
Enter your email address below and we will send you a link to reset
58-
your password.
59-
</p>
60-
)}
61-
<div>
62-
<TextField
63-
fullWidth
64-
name="email"
65-
type="email"
66-
value={email}
67-
variant="secondary"
68-
onChange={handleEmailChange}
69-
isDisabled={isAuthenticating}
70-
>
71-
<Label>Email</Label>
72-
<Input placeholder="me@email.com" />
73-
</TextField>
74-
{mode === 'reset-password' && (
75-
<div className="text-right">
76-
<CustomButton
77-
onPress={goBackToLogin}
78-
className="h-auto bg-transparent p-0 text-gray-400 hover:text-gray-700"
79-
>
80-
Back to login
81-
</CustomButton>
82-
</div>
83-
)}
84-
</div>
85-
{mode === 'register' && (
86-
<TextField
87-
fullWidth
88-
name="name"
89-
value={name}
90-
variant="secondary"
91-
onChange={handleNameChange}
92-
isDisabled={isAuthenticating}
93-
>
94-
<Label>Name</Label>
95-
<Input placeholder="Optional" />
96-
</TextField>
97-
)}
98-
{['login', 'register'].includes(mode) && (
41+
{!!description && <p className="text-muted text-sm">{description}</p>}
42+
<TextField
43+
fullWidth
44+
name="email"
45+
type="email"
46+
value={email}
47+
variant="secondary"
48+
onChange={handleEmailChange}
49+
isDisabled={isAuthenticating}
50+
>
51+
<Label className="h-6">Email</Label>
52+
<Input autoComplete="email" placeholder="me@email.com" />
53+
</TextField>
54+
{withPassword && (
9955
<PasswordInput
10056
value={password}
10157
label="Password"
10258
variant="secondary"
59+
canReset={canReset}
10360
isDisabled={isAuthenticating}
10461
onChange={handlePasswordChange}
105-
onReset={
106-
mode === 'login'
107-
? () => {
108-
return onModeChange('reset-password');
109-
}
110-
: undefined
111-
}
62+
autoComplete={passwordAutoComplete}
11263
/>
11364
)}
11465
</div>
115-
<div className="mt-4 flex justify-end gap-2">
116-
<CustomButton
117-
variant="secondary"
118-
onPress={handleCancel}
119-
isDisabled={isAuthenticating}
120-
>
121-
Cancel
122-
</CustomButton>
123-
<CustomButton
124-
type="submit"
125-
variant="primary"
126-
data-testid="submit-button"
127-
isPending={isAuthenticating}
128-
>
129-
{submitButtonLabel}
130-
</CustomButton>
131-
</div>
66+
<CustomButton
67+
fullWidth
68+
type="submit"
69+
className="mt-6"
70+
variant="primary"
71+
data-testid="submit-button"
72+
isPending={isAuthenticating}
73+
>
74+
{submitLabel}
75+
</CustomButton>
76+
{!!footer && (
77+
<div className="text-muted mt-4 text-center text-sm">{footer}</div>
78+
)}
13279
</form>
13380
);
13481
};

0 commit comments

Comments
 (0)