diff --git a/client/src/components/AuthClient.ts b/client/src/components/AuthClient.ts index bde57efc1..30056048e 100644 --- a/client/src/components/AuthClient.ts +++ b/client/src/components/AuthClient.ts @@ -47,8 +47,12 @@ const clearUser = () => (_user = undefined); * Authenticates a user with the given email and password. * Creates an account for this user if one does not already exist. */ -const register = async (username: string, password: string) => { - const json = await postAuthRequest(`${BASE_URL}auth/register`, { username, password }); +const register = async (username: string, password: string, userType: string) => { + const json = await postAuthRequest(`${BASE_URL}auth/register`, { + username, + password, + user_type: userType, + }); fetchUser(); return json; }; diff --git a/client/src/components/EmailAlertSignup.tsx b/client/src/components/EmailAlertSignup.tsx index 023fedf43..751fc913d 100644 --- a/client/src/components/EmailAlertSignup.tsx +++ b/client/src/components/EmailAlertSignup.tsx @@ -1,7 +1,6 @@ import React, { Fragment, useContext, useState } from "react"; import { withI18n, withI18nProps, I18n } from "@lingui/react"; -import { t } from "@lingui/macro"; import { Trans } from "@lingui/macro"; import Login from "./Login"; import { UserContext } from "./UserContext"; @@ -11,7 +10,8 @@ import { LocaleLink as Link } from "../i18n"; import "styles/EmailAlertSignup.css"; import { JustfixUser } from "state-machine"; import AuthClient from "./AuthClient"; -import { AlertIconOutline, SubscribedIcon } from "./Icons"; +import { SubscribedIcon } from "./Icons"; +import { Alert } from "./Alert"; import Modal from "./Modal"; const SUBSCRIPTION_LIMIT = 15; @@ -49,21 +49,19 @@ const BuildingSubscribeWithoutI18n = (props: BuildingSubscribeProps) => { const showEmailVerification = (i18n: any) => { return (
-
-
- - Email verification required -
-
- {i18n._(t`Click the link we sent to ${email}. It may take a few minutes to arrive.`)} -
- -
+ + Verify your email to start receiving updates. + + + Click the link we sent to {email}. It may take a few minutes to arrive. + + Didn’t get the link? +
); }; @@ -71,7 +69,7 @@ const BuildingSubscribeWithoutI18n = (props: BuildingSubscribeProps) => { {({ i18n }) => ( <> -
+
{!(subscriptions && !!subscriptions?.find((s) => s.bbl === bbl)) ? (
- { const { bbl, housenumber, streetname, zip, boro } = props; const userContext = useContext(UserContext); const { user } = userContext; - const [showVerifyModal, setShowVerifyModal] = useState(false); + const [loginRegisterInProgress, setLoginRegisterInProgress] = useState(false); return ( <> @@ -133,10 +130,7 @@ const EmailAlertSignupWithoutI18n = (props: EmailAlertProps) => {
{({ i18n }) => ( -
+
- {!user ? ( - -
- - Each weekly email includes HPD Complaints, HPD Violations, and Eviction - Filings. - -
- { - userContext.subscribe(bbl, housenumber, streetname, zip, boro, user); - !user.verified && setShowVerifyModal(true); - }} - /> -
- ) : ( + {user && !loginRegisterInProgress ? ( + ) : ( + { + userContext.subscribe(bbl, housenumber, streetname, zip, boro, user); + }} + /> )}
- setShowVerifyModal(false)} - > - Verify your email to start receiving updates - {i18n._( - t`Click the link we sent to ${user?.email}. It may take a few minutes to arrive.` - )} -
-
- - Once your email has been verified, you’ll be signed up for Data Updates. - -
-
- -
)} diff --git a/client/src/components/EmailInput.tsx b/client/src/components/EmailInput.tsx new file mode 100644 index 000000000..a02cbcc41 --- /dev/null +++ b/client/src/components/EmailInput.tsx @@ -0,0 +1,74 @@ +import { ChangeEvent, forwardRef } from "react"; +import { t } from "@lingui/macro"; +import { I18n } from "@lingui/core"; +import { withI18n } from "@lingui/react"; +import { AlertIcon } from "./Icons"; + +import "styles/EmailInput.css"; +import "styles/_input.scss"; +import classNames from "classnames"; + +interface EmailInputProps extends React.ComponentPropsWithoutRef<"input"> { + i18n: I18n; + email: string; + error: boolean; + setError: React.Dispatch>; + showError: boolean; + onChange: (e: ChangeEvent) => void; + i18nHash?: string; +} + +const EmailInputWithoutI18n = forwardRef( + ({ i18n, i18nHash, email, error, setError, showError, onChange, ...props }, ref) => { + const isBadEmailFormat = (value: string) => { + /* valid email regex rules + alpha numeric characters are ok, upper/lower case agnostic + username: leading \_ ok, chars \_\.\-\+ ok in all other positions + domain name: chars \.\- ok as long as not leading. must end in a \. and at least two alphabet chars */ + const pattern = + "^([a-zA-Z0-9_]+[a-zA-Z0-9+_.-]+@[a-zA-Z0-9]+[a-zA-Z0-9.-]+[a-zA-Z0-9]+.[a-zA-Z]{2,})$"; + + // HTML input element has loose email validation requirements, so we check the input against a custom regex + const passStrictRegex = value.match(pattern); + const passAutoValidation = document.querySelectorAll("input:invalid").length === 0; + + return !passAutoValidation || !passStrictRegex; + }; + + const handleChange = (e: ChangeEvent) => { + onChange(e); + const emailIsInvalid = isBadEmailFormat(e.target.value); + setError(emailIsInvalid); + }; + + return ( +
+
+ +
+ {showError && error && ( +
+ + + {i18n._(t`Please enter a valid email address.`)} + +
+ )} +
+ +
+
+ ); + } +); + +const EmailInput = withI18n({ withHash: false })(EmailInputWithoutI18n); + +export default EmailInput; diff --git a/client/src/components/Icons.tsx b/client/src/components/Icons.tsx index 8d625325b..c2ae08635 100644 --- a/client/src/components/Icons.tsx +++ b/client/src/components/Icons.tsx @@ -33,15 +33,15 @@ export const CheckIcon = (props: SVGProps) => ( export const SubscribedIcon = (props: SVGProps) => ( - - + + ); diff --git a/client/src/components/Login.tsx b/client/src/components/Login.tsx index 5dc15e8b7..4db9bd5e4 100644 --- a/client/src/components/Login.tsx +++ b/client/src/components/Login.tsx @@ -1,23 +1,31 @@ import React, { useState, useContext } from "react"; - -import "styles/Login.css"; -import "styles/_input.scss"; - +import { Trans, t } from "@lingui/macro"; import { I18n } from "@lingui/core"; import { withI18n } from "@lingui/react"; -import { Trans, t } from "@lingui/macro"; +import classNames from "classnames"; + +import AuthClient from "./AuthClient"; +import { JustfixUser } from "state-machine"; import { UserContext } from "./UserContext"; +import { useInput } from "util/helpers"; import PasswordInput from "./PasswordInput"; -import { JustfixUser } from "state-machine"; -import AuthClient from "./AuthClient"; +import EmailInput from "./EmailInput"; +import UserTypeInput from "./UserTypeInput"; import { Alert } from "./Alert"; -import { AlertIcon, InfoIcon } from "./Icons"; +import { InfoIcon } from "./Icons"; import Modal from "./Modal"; -export enum LoginState { - Default, +import "styles/Login.css"; +import "styles/UserTypeInput.css"; +import "styles/_input.scss"; + +enum Step { + CheckEmail, Login, - Register, + RegisterAccount, + RegisterUserType, + VerifyEmail, + VerifyEmailReminder, } type LoginProps = { @@ -25,96 +33,67 @@ type LoginProps = { onBuildingPage?: boolean; onSuccess?: (user: JustfixUser) => void; handleRedirect?: () => void; + registerInModal?: boolean; + setLoginRegisterInProgress?: React.Dispatch>; }; const LoginWithoutI18n = (props: LoginProps) => { - const { i18n, onBuildingPage, onSuccess, handleRedirect } = props; - const userContext = useContext(UserContext); + const { + i18n, + onBuildingPage, + onSuccess, + handleRedirect, + registerInModal, + setLoginRegisterInProgress, + } = props; - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [header, setHeader] = useState("Log in / sign up"); - const [subheader, setSubheader] = useState(""); - const [loginState, setLoginState] = useState(LoginState.Default); - const isDefaultState = loginState === LoginState.Default; - const isRegisterState = loginState === LoginState.Register; + const userContext = useContext(UserContext); + const [showRegisterModal, setShowRegisterModal] = useState(false); const [showInfoModal, setShowInfoModal] = useState(false); - const [emailFormatError, setEmailFormatError] = useState(false); - const [emptyAuthError, setEmptyAuthError] = useState(false); + + const [step, setStep] = useState(Step.CheckEmail); + const isCheckEmailStep = step === Step.CheckEmail; + const isLoginStep = step === Step.Login; + const isRegisterAccountStep = step === Step.RegisterAccount; + const isRegisterUserTypeStep = step === Step.RegisterUserType; + const isVerifyEmailStep = step === Step.VerifyEmail; + const isVerifyEmailReminderStep = step === Step.VerifyEmailReminder; + + const { + value: email, + error: emailError, + showError: showEmailError, + setError: setEmailError, + setShowError: setShowEmailError, + onChange: onChangeEmail, + } = useInput(""); + const { + value: password, + error: passwordError, + showError: showPasswordError, + setError: setPasswordError, + setShowError: setShowPasswordError, + onChange: onChangePassword, + } = useInput(""); + const { + value: userType, + error: userTypeError, + showError: userShowUserTypeError, + setValue: setUserType, + setError: setUserTypeError, + setShowError: setShowUserTypeError, + onChange: onChangeUserType, + } = useInput(""); + const [invalidAuthError, setInvalidAuthError] = useState(false); const [existingUserError, setExistingUserError] = useState(false); const resetErrorStates = () => { - setEmptyAuthError(false); setInvalidAuthError(false); setExistingUserError(false); }; - const toggleLoginState = (endState: LoginState) => { - if (endState === LoginState.Register) { - setLoginState(LoginState.Register); - if (!onBuildingPage) { - setHeader("Sign up"); - setSubheader("With an account you can save buildings and get weekly updates"); - } - } else if (endState === LoginState.Login) { - setLoginState(LoginState.Login); - if (!onBuildingPage) { - setHeader("Log in"); - setSubheader(""); - } - } - }; - - const handleEmailSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (isDefaultState && !!username && !emailFormatError) { - const existingUser = await AuthClient.isEmailAlreadyUsed(username); - - if (existingUser) { - setExistingUserError(true); - toggleLoginState(LoginState.Login); - } else { - toggleLoginState(LoginState.Register); - } - } - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - resetErrorStates(); - - if (!username || !password) { - setEmptyAuthError(true); - return; - } - - const existingUser = await AuthClient.isEmailAlreadyUsed(username); - if (existingUser) { - if (isRegisterState) { - setExistingUserError(true); - } else { - const error = await userContext.login(username, password, onSuccess); - if (!!error) { - setInvalidAuthError(true); - } else { - handleRedirect && handleRedirect(); - } - } - } else { - const error = isRegisterState - ? await userContext.register(username, password, onSuccess) - : await userContext.login(username, password, onSuccess); - - if (!!error) { - setInvalidAuthError(true); - } else { - handleRedirect && handleRedirect(); - } - } - }; - const renderPageLevelAlert = ( type: "error" | "success" | "info", message: string, @@ -130,10 +109,7 @@ const LoginWithoutI18n = (props: LoginProps) => { > {message} {showLogin && ( - )} @@ -148,30 +124,18 @@ const LoginWithoutI18n = (props: LoginProps) => { case invalidAuthError: alertMessage = i18n._(t`The email and/or password you entered is incorrect.`); return renderPageLevelAlert("error", alertMessage); - case emptyAuthError: - alertMessage = i18n._(t`The email and/or password cannot be blank.`); - return renderPageLevelAlert("error", alertMessage); case existingUserError: - if (isRegisterState) { + if (isRegisterAccountStep) { alertMessage = i18n._(t`That email is already used.`); // show login button in alert - return renderPageLevelAlert("error", alertMessage, !onBuildingPage); - } else if (onBuildingPage) { + return renderPageLevelAlert("error", alertMessage, !onBuildingPage || showRegisterModal); + } else if (isLoginStep && onBuildingPage && showRegisterModal) { alertMessage = i18n._(t`Your email is associated with an account. Log in below.`); return renderPageLevelAlert("info", alertMessage); } } }; - const renderHeader = () => { - return ( - <> -

{i18n._(t`${header}`)}

-
{i18n._(t`${subheader}`)}
- - ); - }; - const renderFooter = () => { return (
@@ -186,13 +150,10 @@ const LoginWithoutI18n = (props: LoginProps) => {
- {isRegisterState ? ( + {isRegisterAccountStep ? ( <> Already have an account? - @@ -201,7 +162,7 @@ const LoginWithoutI18n = (props: LoginProps) => { Don't have an account? @@ -231,79 +192,287 @@ const LoginWithoutI18n = (props: LoginProps) => { ); }; - const isBadEmailFormat = () => { - /* valid email regex rules - alpha numeric characters are ok, upper/lower case agnostic - username: leading \_ ok, chars \_\.\-\+ ok in all other positions - domain name: chars \.\- ok as long as not leading. must end in a \. and at least two alphabet chars */ - const pattern = - "^([a-zA-Z0-9_]+[a-zA-Z0-9+_.-]+@[a-zA-Z0-9]+[a-zA-Z0-9.-]+[a-zA-Z0-9]+.[a-zA-Z]{2,})$"; - const input = document.getElementById("email-input") as HTMLElement; - const inputValue = (input as HTMLInputElement).value; - - // HTML input element has loose email validation requirements, so we check the input against a custom regex - const passStrictRegex = inputValue.match(pattern); - const passAutoValidation = document.querySelectorAll("input:invalid").length === 0; - - if (!passAutoValidation || !passStrictRegex) { - setEmailFormatError(true); - input.className = input.className + " invalid"; - } else { - setEmailFormatError(false); - input.className = input.className.split(" ")[0]; + const renderVerifyEmail = () => { + return ( +
+

{i18n._(t`Click the link we sent to ${email}. It may take a few minutes to arrive.`)}

+ + Didn’t get the link? + + +
+ ); + }; + + const renderVerifyEmailReminder = () => { + return ( + <> + Verify your email to start receiving updates + {i18n._(t`Click the link we sent to ${email}. It may take a few minutes to arrive.`)} +
+
+ Once your email has been verified, you’ll be signed up for Data Updates. +
+
+ + + ); + }; + + const onEmailSubmit = async () => { + !!setLoginRegisterInProgress && setLoginRegisterInProgress(true); + + if (!email) { + if (!onBuildingPage || showRegisterModal) { + setEmailError(true); + setShowEmailError(true); + } + registerInModal && setShowRegisterModal(true); + return; + } + + if (!!email && emailError) { + setEmailError(true); + setShowEmailError(true); + return; + } + + if (!!email && !emailError) { + const existingUser = await AuthClient.isEmailAlreadyUsed(email); + + if (existingUser) { + setStep(Step.Login); + setExistingUserError(true); + } else { + setStep(Step.RegisterAccount); + if (registerInModal && !showRegisterModal) { + setShowRegisterModal(true); + } + } } }; - return ( -
- {renderAlert()} - {!onBuildingPage && renderHeader()} -
- Email address - {emailFormatError && ( - - - Please enter a valid email address. - + const onLoginSubmit = async () => { + resetErrorStates(); + + if (!email || emailError) { + setEmailError(true); + setShowEmailError(true); + return; + } + + if (!password) { + setPasswordError(true); + setShowPasswordError(true); + return; + } + + // context doesn't update immediately so need to reurn user to check verified status + const resp = await userContext.login(email, password, onSuccess); + + if (!!resp?.error) { + setInvalidAuthError(true); + return; + } + + !!setLoginRegisterInProgress && setLoginRegisterInProgress(false); + + if (!onBuildingPage) { + handleRedirect && handleRedirect(); + return; + } + }; + + const onAccountSubmit = async () => { + if (!email || emailError) { + setEmailError(true); + setShowEmailError(true); + return; + } + + const existingUser = await AuthClient.isEmailAlreadyUsed(email); + if (existingUser) { + setStep(Step.Login); + setExistingUserError(true); + return; + } + + if (!password || passwordError) { + setPasswordError(true); + setShowPasswordError(true); + return; + } + + setStep(Step.RegisterUserType); + }; + + const onUserTypeSubmit = async () => { + if (!userType || userTypeError) { + setUserTypeError(true); + setShowUserTypeError(true); + return; + } + + const resp = await userContext.register(email, password, userType, onSuccess); + + if (!!resp?.error) { + setInvalidAuthError(true); + setStep(Step.RegisterAccount); + return; + } + + if (!onBuildingPage) { + !!setLoginRegisterInProgress && setLoginRegisterInProgress(false); + handleRedirect && handleRedirect(); + return; + } + + if (!registerInModal) { + !!setLoginRegisterInProgress && setLoginRegisterInProgress(false); + } + + setStep(Step.VerifyEmail); + }; + + let headerText = ""; + let onSubmit = () => {}; + let submitButtonText = ""; + switch (step) { + case Step.CheckEmail: + headerText = onBuildingPage + ? i18n._(t`Get weekly Data Updates for complaints, violations, and evictions.`) + : i18n._(t`Log in / sign up`); + onSubmit = onEmailSubmit; + submitButtonText = + !onBuildingPage || showRegisterModal ? i18n._(t`Submit`) : i18n._(t`Get updates`); + break; + case Step.Login: + headerText = !onBuildingPage + ? i18n._(t`Log in`) + : showRegisterModal + ? i18n._(t`Get weekly Data Updates for complaints, violations, and evictions.`) + : i18n._(t`Log in to start getting updates for this building.`); + onSubmit = onLoginSubmit; + submitButtonText = i18n._(t`Log in`); + break; + case Step.RegisterAccount: + headerText = onBuildingPage + ? i18n._(t`Create a password to start receiving Data Updates`) + : i18n._(t`Sign up`); + onSubmit = onAccountSubmit; + submitButtonText = i18n._(t`Next`); + break; + case Step.RegisterUserType: + headerText = i18n._(t`Which best describes you?`); + onSubmit = onUserTypeSubmit; + submitButtonText = "Sign up"; + break; + case Step.VerifyEmail: + headerText = i18n._(t`Almost done. Verify your email to start receiving updates.`); + break; + } + + const renderLoginFlow = () => { + return ( +
+ {(!onBuildingPage || showRegisterModal) && ( +

{headerText}

)} - { - setUsername(e.target.value); - }} - onBlur={isBadEmailFormat} - value={username} - // note: required={true} removed bc any empty state registers as invalid state - /> - {!isDefaultState && ( - + {onBuildingPage && !showRegisterModal && ( +
{headerText}
)} - - - {onBuildingPage && renderFooter()} -
+ {renderAlert()} + {!isVerifyEmailStep && !isVerifyEmailReminderStep && ( +
{ + e.preventDefault(); + resetErrorStates(); + onSubmit(); + }} + > + {(isCheckEmailStep || isLoginStep || isRegisterAccountStep) && ( + + )} + {(isLoginStep || isRegisterAccountStep) && ( + + )} + {isRegisterUserTypeStep && ( + + )} +
+ +
+ + )} + {isVerifyEmailStep && renderVerifyEmail()} + {isVerifyEmailReminderStep && renderVerifyEmailReminder()} + {(isLoginStep || isRegisterAccountStep) && renderFooter()} +
+ ); + }; + + return ( + <> + {(!showRegisterModal || isCheckEmailStep || isLoginStep || isVerifyEmailStep) && + renderLoginFlow()} + {registerInModal && ( + { + resetErrorStates(); + setShowEmailError(false); + setShowRegisterModal(false); + setStep(Step.CheckEmail); + !!setLoginRegisterInProgress && setLoginRegisterInProgress(false); + }} + > + {renderLoginFlow()} + + )} + ); }; -const Login = withI18n()(LoginWithoutI18n); +export const Login = withI18n()(LoginWithoutI18n); export default Login; diff --git a/client/src/components/PasswordInput.tsx b/client/src/components/PasswordInput.tsx index c7a663a34..7c9fa0264 100644 --- a/client/src/components/PasswordInput.tsx +++ b/client/src/components/PasswordInput.tsx @@ -1,5 +1,5 @@ /* eslint-disable no-useless-escape */ -import React, { Fragment, useState } from "react"; +import React, { ChangeEvent, forwardRef, useState } from "react"; import "styles/Password.css"; import "styles/_input.scss"; @@ -10,6 +10,7 @@ import { createWhoOwnsWhatRoutePaths } from "routes"; import { I18n } from "@lingui/core"; import { t } from "@lingui/macro"; import { HideIcon, ShowIcon } from "./Icons"; +import classNames from "classnames"; type PasswordRule = { regex: RegExp; @@ -24,73 +25,104 @@ const passwordRules: PasswordRule[] = [ }, ]; -export const validatePassword = (password: string) => { +const isBadPasswordFormat = (password: string) => { let valid = true; passwordRules.forEach((rule) => (valid = valid && !!password.match(rule.regex))); - return valid; + return !valid; }; -type PasswordInputProps = { +interface PasswordInputProps extends React.ComponentPropsWithoutRef<"input"> { i18n: I18n; labelText: string; + password: string; + onChange: (e: ChangeEvent) => void; + error: boolean; + showError: boolean; + setError: React.Dispatch>; username?: string; - onChange?: (password: string) => void; showPasswordRules?: boolean; showForgotPassword?: boolean; -}; + i18nHash?: string; +} -const PasswordInputWithoutI18n = (props: PasswordInputProps) => { - const { account } = createWhoOwnsWhatRoutePaths(); +const PasswordInputWithoutI18n = forwardRef( + ( + { + i18n, + i18nHash, + labelText, + username, + password, + error, + setError, + showError, + onChange, + showPasswordRules, + showForgotPassword, + id, + ...props + }, + ref + ) => { + const { account } = createWhoOwnsWhatRoutePaths(); - const { i18n, labelText, username, onChange, showPasswordRules, showForgotPassword } = props; - const [password, setPassword] = useState(""); - const [showPassword, setShowPassword] = useState(false); + const [showPassword, setShowPassword] = useState(false); - const handlePasswordChange = (e: React.ChangeEvent) => { - setPassword(e.target.value); - if (onChange) onChange(e.target.value); - }; + const handleChange = (e: ChangeEvent) => { + onChange(e); + const passwordIsInvalid = isBadPasswordFormat(e.target.value); + setError(passwordIsInvalid); + }; - return ( - -
- - {showForgotPassword && ( - - Forgot your password? - + return ( +
+
+ + {showForgotPassword && ( + + Forgot your password? + + )} +
+ {showPasswordRules && ( +
+ {passwordRules.map((rule, i) => { + const ruleClass = !!password + ? password.match(rule.regex) + ? "valid" + : "invalid" + : ""; + return ( + + {rule.label} + + ); + })} +
)} -
- {showPasswordRules && ( -
- {passwordRules.map((rule, i) => { - const ruleClass = !!password ? (password.match(rule.regex) ? "valid" : "invalid") : ""; - return ( - - {rule.label} - - ); - })} +
+ +
- )} -
- -
- - ); -}; + ); + } +); PasswordInputWithoutI18n.defaultProps = { showPasswordRules: false, diff --git a/client/src/components/UserContext.tsx b/client/src/components/UserContext.tsx index a83897c60..ebef8438a 100644 --- a/client/src/components/UserContext.tsx +++ b/client/src/components/UserContext.tsx @@ -3,18 +3,24 @@ import { JustfixUser } from "state-machine"; import AuthClient from "./AuthClient"; import { authRequiredPaths } from "routes"; +type UserOrError = { + user?: JustfixUser; + error?: string; +}; + export type UserContextProps = { user?: JustfixUser; register: ( username: string, password: string, + userType: string, onSuccess?: (user: JustfixUser) => void - ) => Promise; + ) => Promise; login: ( username: string, password: string, onSuccess?: (user: JustfixUser) => void - ) => Promise; + ) => Promise; logout: (fromPath: string) => void; subscribe: ( bbl: string, @@ -35,6 +41,7 @@ const initialState: UserContextProps = { register: async ( username: string, password: string, + userType: string, onSuccess?: (user: JustfixUser) => void ) => {}, login: async (username: string, password: string, onSuccess?: (user: JustfixUser) => void) => {}, @@ -75,8 +82,13 @@ export const UserContextProvider = ({ children }: { children: React.ReactNode }) }, []); const register = useCallback( - async (username: string, password: string, onSuccess?: (user: JustfixUser) => void) => { - const response = await AuthClient.register(username, password); + async ( + username: string, + password: string, + userType: string, + onSuccess?: (user: JustfixUser) => void + ) => { + const response = await AuthClient.register(username, password, userType); if (!response.error && response.user) { const _user = { ...response.user, @@ -87,8 +99,9 @@ export const UserContextProvider = ({ children }: { children: React.ReactNode }) }; setUser(_user); if (onSuccess) onSuccess(_user); + return { user: _user }; } else { - return response.error_description; + return { error: response.error_description }; } }, [] @@ -108,8 +121,9 @@ export const UserContextProvider = ({ children }: { children: React.ReactNode }) }; setUser(_user); if (onSuccess) onSuccess(_user); + return { user: _user }; } else { - return response.error; + return { error: response.error }; } }, [] diff --git a/client/src/components/UserSettingField.tsx b/client/src/components/UserSettingField.tsx index c8a1c5c9e..bd29cd0a2 100644 --- a/client/src/components/UserSettingField.tsx +++ b/client/src/components/UserSettingField.tsx @@ -5,6 +5,7 @@ import { t, Trans } from "@lingui/macro"; import "styles/EmailAlertSignup.css"; import PasswordInput from "./PasswordInput"; +import { useInput } from "util/helpers"; type PasswordSettingFieldProps = withI18nProps & { onSubmit: (currentPassword: string, newPassword: string) => void; @@ -12,39 +13,46 @@ type PasswordSettingFieldProps = withI18nProps & { const PasswordSettingFieldWithoutI18n = (props: PasswordSettingFieldProps) => { const { i18n, onSubmit } = props; - const [newPassword, setNewPassword] = useState(""); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [currentPassword, setCurrentPassword] = useState(""); + const { + value: currentPassword, + error: currentPasswordError, + showError: showCurrentPasswordError, + setError: setCurrentPasswordError, + onChange: onChangeCurrentPassword, + } = useInput(""); + const { + value: newPassword, + error: newPasswordError, + showError: showNewPasswordError, + setError: setNewPasswordError, + onChange: onChangeNewPassword, + } = useInput(""); // const [showCurrentPassword, setShowCurrentPassword] = useState(false); - // const handleCurrentPasswordChange = (e: React.ChangeEvent) => { - // setCurrentPassword(e.target.value); - // }; - const handleSubmit = () => { onSubmit(currentPassword, newPassword); }; return ( - Password - - - {/*
- - -
*/} +
); diff --git a/client/src/components/UserTypeInput.tsx b/client/src/components/UserTypeInput.tsx new file mode 100644 index 000000000..935f9e180 --- /dev/null +++ b/client/src/components/UserTypeInput.tsx @@ -0,0 +1,153 @@ +import { ChangeEvent, useState } from "react"; +import { Trans, t } from "@lingui/macro"; +import { I18n } from "@lingui/core"; +import { withI18n } from "@lingui/react"; +import { AlertIcon } from "./Icons"; +import classNames from "classnames"; + +type UserTypeInputProps = { + i18n: I18n; + userType: string; + error: boolean; + showError: boolean; + setError: React.Dispatch>; + setUserType: React.Dispatch>; + onChange: (e: ChangeEvent) => void; + required?: boolean; +}; + +const UserTypeInputWithoutI18n = (props: UserTypeInputProps) => { + const { i18n, userType, error, showError, setError, setUserType, onChange, required } = props; + + const [activeRadio, setActiveRadio] = useState(""); + + const handleRadioChange = (e: ChangeEvent) => { + const value = e.target.value; + setActiveRadio(value); + if (value === USER_TYPES.other) { + setUserType(""); + setError(true); + } else { + setUserType(value); + setError(false); + } + }; + + const handleTextChange = (e: ChangeEvent) => { + onChange(e); + const value = e.target.value; + setError(!value); + }; + + const USER_TYPES = { + tenant: i18n._(t`Tenant`), + organizer: i18n._(t`Organizer`), + advocate: i18n._(t`Advocate`), + legal: i18n._(t`Legal Worker`), + government: i18n._(t`Government Worker (Non-Lawyer)`), + other: i18n._(t`Other`), + }; + + return ( +
+ {showError && error && activeRadio !== USER_TYPES.other && ( + + + Please select an option. + + )} +
+ + + + + + + + + + + + +
+ {activeRadio === USER_TYPES.other && ( +
+ {showError && error && ( + + + Please enter a response. + + )} + +
+ )} +
+ ); +}; + +const UserTypeInput = withI18n()(UserTypeInputWithoutI18n); + +export default UserTypeInput; diff --git a/client/src/containers/ResetPasswordPage.tsx b/client/src/containers/ResetPasswordPage.tsx index 04e062e46..fde595b66 100644 --- a/client/src/containers/ResetPasswordPage.tsx +++ b/client/src/containers/ResetPasswordPage.tsx @@ -8,6 +8,7 @@ import { Trans, t } from "@lingui/macro"; import { UserContext } from "components/UserContext"; import PasswordInput from "components/PasswordInput"; +import { useInput } from "util/helpers"; const ResetPasswordPage = withI18n()((props: withI18nProps) => { const { i18n } = props; @@ -15,8 +16,14 @@ const ResetPasswordPage = withI18n()((props: withI18nProps) => { const params = new URLSearchParams(search); const [requestSent, setRequestSent] = React.useState(false); - const [value, setValue] = React.useState(""); const userContext = useContext(UserContext); + const { + value: password, + error: passwordError, + showError: showPasswordError, + setError: setPasswordError, + onChange: onChangePassword, + } = useInput(""); const delaySeconds = 5; const baseUrl = window.location.origin; @@ -38,7 +45,7 @@ const ResetPasswordPage = withI18n()((props: withI18nProps) => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - await userContext.resetPassword(params.get("token") || "", value); + await userContext.resetPassword(params.get("token") || "", password); setRequestSent(true); }; @@ -49,13 +56,19 @@ const ResetPasswordPage = withI18n()((props: withI18nProps) => { {!requestSent ? ( <> Reset your password -
+ - + ) : ( diff --git a/client/src/styles/AccountSettingsPage.scss b/client/src/styles/AccountSettingsPage.scss index ba9b310ce..84631c7c6 100644 --- a/client/src/styles/AccountSettingsPage.scss +++ b/client/src/styles/AccountSettingsPage.scss @@ -42,7 +42,13 @@ form { display: flex; flex-direction: column; - margin: 2rem 1rem; + margin: 3.2rem 1.6rem; + gap: 2.4rem; + + .password-input-field { + display: flex; + flex-direction: column; + } p { margin: -0.63rem 0 0.37rem 0; @@ -60,7 +66,6 @@ .user-setting-actions { display: flex; justify-content: right; - margin-top: 1rem; @include for-phone-only() { justify-content: center; @@ -82,10 +87,6 @@ margin-bottom: 1rem; } - .password-input-label { - margin-bottom: -0.63rem !important; - } - .password-input-rules { display: flex; flex-direction: column; diff --git a/client/src/styles/DetailView.scss b/client/src/styles/DetailView.scss index fa1126e63..fe3cb1791 100644 --- a/client/src/styles/DetailView.scss +++ b/client/src/styles/DetailView.scss @@ -123,7 +123,7 @@ } .DetailView__mobilePortfolioView { - padding: 0.81rem 1.25rem; + padding: 0.8125rem 1.25rem; display: none; @include for-phone-only() { diff --git a/client/src/styles/EmailAlertSignup.scss b/client/src/styles/EmailAlertSignup.scss index 7adb98cfe..ac0ffcee7 100644 --- a/client/src/styles/EmailAlertSignup.scss +++ b/client/src/styles/EmailAlertSignup.scss @@ -35,13 +35,20 @@ } } + .table-small-font { + padding: 0 !important; + } + .table-content { - margin: 0.63rem 0.63rem 1.06rem 0.63rem !important; + display: flex; + flex-direction: column; + gap: 1.5rem; + padding: 1.5rem; + margin: 0 !important; } .email-description { text-align: left; - margin: 0.63rem 0 !important; line-height: 117%; } @@ -55,6 +62,7 @@ display: flex; flex-direction: column; text-align: left; + gap: 1.5rem; button { align-self: center; @@ -63,15 +71,10 @@ .status-title { display: flex; align-items: center; - margin-bottom: 1.25rem; svg { margin-right: 0.63rem; } } - - .status-description { - margin-bottom: 1.25rem; - } } } diff --git a/client/src/styles/EmailInput.scss b/client/src/styles/EmailInput.scss new file mode 100644 index 000000000..bdbccda29 --- /dev/null +++ b/client/src/styles/EmailInput.scss @@ -0,0 +1,61 @@ +@import "_vars.scss"; +@import "_typography.scss"; + +.email-input-field { + .email-input-label { + display: flex; + align-items: center; + justify-content: space-between; + + label { + @include body-standard; + // vars marked !important bc typography mixin is not available for inconsolata + // font-weight: 600 !important; + font-family: $wow-font !important; + font-size: 0.8125rem !important; + line-height: 120%; + color: $justfix-black; + + text-align: left; + margin-bottom: 0.3125rem; + } + + a { + color: $justfix-black; + font-size: 0.8125rem; + } + } + + .email-input { + display: flex; + + input { + flex: 1; + + &.invalid { + border-color: $justfix-orange; + } + + &.invalid:focus { + border-color: $justfix-black; + } + } + } + + .email-input-errors { + display: flex; + flex-direction: column; + + span { + display: flex; + align-items: center; + font-size: 0.8125rem; + color: #ba4300; + margin-bottom: 0.3125rem; + + svg { + margin-right: 0.47rem; + } + } + } +} diff --git a/client/src/styles/ForgotPasswordPage.scss b/client/src/styles/ForgotPasswordPage.scss index e23bdb800..ce2fe7c8c 100644 --- a/client/src/styles/ForgotPasswordPage.scss +++ b/client/src/styles/ForgotPasswordPage.scss @@ -2,30 +2,34 @@ @import "_typography.scss"; .ForgotPasswordPage { - .page-title { - margin-top: 0.94rem; + .page-container { + display: flex; + flex-direction: column; + gap: 1.5rem; } h4 { font-size: 1.5rem; + margin: 0; } h5 { font-size: 1rem; + margin: 0; } label { @include desktop-eyebrow(); text-align: left; - margin-top: 0.63rem; - margin-bottom: 0.31rem; + margin-top: 0.39rem; + margin-bottom: 0.19rem; } input[type="submit"] { - padding: 1rem 2rem !important; + padding: 0.63rem 1.25rem !important; align-self: center; - margin: 1.63rem auto 0.37rem auto !important; + margin: 1.02rem auto 0.23rem auto !important; } .request-sent-success { @@ -34,11 +38,11 @@ align-items: center; svg { - margin: 0.63rem 0 1.56rem 0; + margin: 0.39rem 0 0.98rem 0; } .button.is-text { - margin-top: 0.63rem; + margin-top: 0.39rem; display: block !important; color: $justfix-black; } diff --git a/client/src/styles/Login.scss b/client/src/styles/Login.scss index ebbe9396f..58b8ba3ad 100644 --- a/client/src/styles/Login.scss +++ b/client/src/styles/Login.scss @@ -5,74 +5,44 @@ .Login, .ForgotPasswordPage, .ResetPasswordPage { + display: flex; + flex-direction: column; + gap: 1.5rem; + .input-group { display: flex; flex-direction: column; - margin-top: 0.63rem; + gap: 1.5rem; - #input-field-error { + .submit-button-group { display: flex; - align-items: center; - font-size: 0.81rem; - color: #ba4300; - margin-bottom: 0.31rem; - svg { - margin-right: 0.47rem; + button[type="submit"], + input[type="submit"] { + padding: 1rem 2rem !important; + align-self: center; + margin: 0 auto !important; } } - - input[type="email"], - .password-input { - margin-bottom: 1.25rem; - } - - input[type="submit"] { - padding: 1rem 2rem !important; - align-self: center; - margin: 0 auto !important; - } - - input:invalid, - .invalid { - border-color: $justfix-orange; - } - - input:focus:invalid, - .invalid:focus { - border-color: $justfix-black; - } } .page-title { - margin-top: 0.94rem; - } - - label { - @include desktop-eyebrow(); - // vars marked !important bc typography mixin is not available for inconsolata - font-weight: 600 !important; - font-family: $wow-font !important; - font-size: 0.81rem !important; - - text-align: left; - margin-top: 0.63rem; - margin-bottom: 0.31rem; + margin-top: 0.59rem; } .building-page-footer { display: flex; flex-direction: column; justify-content: center; + gap: 1.5rem; .privacy-modal { display: flex; justify-content: center; align-items: center; - margin: 1.25rem 0 1.25rem 0; .info-icon { - margin-left: 0.25rem; + margin-left: 0.16rem; width: 18px; &:focus-visible { outline: 2px solid $focus-outline-color; @@ -89,7 +59,7 @@ } .login-response-text { - margin: 0.63rem 0 0.63rem; + margin: 0.39rem 0 0.39rem; } .login-password-label { @@ -104,13 +74,31 @@ h4 { font-size: 1.5rem; + margin-bottom: 0; } h5 { - font-size: 1rem; + font-size: 0.63rem; } .password-input { - margin-top: 0.31rem; + margin-top: 0.3125rem; + } + + .verify-email-container { + color: $justfix-black; + display: flex; + flex-direction: column; + gap: 1.5rem; + + p { + margin: 0; + } + + h4, + .resend-verify-label { + display: flex; + justify-content: center; + } } .jf-alert { @@ -134,11 +122,11 @@ align-items: center; svg { - margin: 0.63rem 0 1.56rem 0; + margin: 0.39rem 0 0.98rem 0; } .button.is-text { - margin-top: 0.63rem; + margin-top: 0.39rem; display: block !important; color: $justfix-black; } diff --git a/client/src/styles/Password.scss b/client/src/styles/Password.scss index 52239d9f2..8f68fa5b7 100644 --- a/client/src/styles/Password.scss +++ b/client/src/styles/Password.scss @@ -1,76 +1,100 @@ @import "_vars.scss"; @import "_typography.scss"; -.password-input-label { - font-weight: 600 !important; - display: flex; - align-items: center; - justify-content: space-between; +.password-input-field { + .password-input-label { + display: flex; + align-items: center; + justify-content: space-between; - a { - color: $justfix-black; - font-size: 0.81rem; - } -} + label { + @include body-standard; + // vars marked !important bc typography mixin is not available for inconsolata + // font-weight: 600 !important; + font-family: $wow-font !important; + font-size: 0.8125rem !important; + line-height: 120%; + color: $justfix-black; -.password-input { - margin-bottom: 0.63rem; - display: flex; + text-align: left; + margin-bottom: 0.31rem; + } - input { - flex: 1; - border-top-right-radius: 0; - border-bottom-right-radius: 0; + a { + color: $justfix-black; + font-size: 0.8125rem; + } } - button { + .password-input { display: flex; - align-items: center; - justify-content: center; - color: $justfix-white; - background-color: $justfix-black; - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; - width: 2.5rem; - } -} -.password-input-rules { - display: flex; - flex-direction: column; -} + input { + flex: 1; + border-top-right-radius: 0; + border-bottom-right-radius: 0; -.password-input-rule { - @include desktop-eyebrow(); - - font-family: $wow-font !important; - font-size: 0.81rem !important; - font-weight: 500 !important; - text-transform: none !important; - color: $justfix-black; - text-align: left; - - &::before { - content: "•"; - display: inline-block; - margin-right: 0.31rem; - vertical-align: top; - } + &:invalid, + &.invalid { + border-color: $justfix-orange; + } - // encoded SVG from https://yoksel.github.io/url-encoder/ - &.invalid:before { - content: url("data:image/svg+xml, %3Csvg width='14' height='14' viewBox='0 0 14 14' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M7 14C3.14 14 0 10.8599 0 7C0 3.14007 3.14007 0 7 0C10.8599 0 14 3.14007 14 7C14 10.84 10.8599 14 7 14Z' fill='%23FF813A' /%3E%3Cpath d='M7 8.75C6.50422 8.75 6.125 8.37612 6.125 7.88729V3.48771C6.125 2.9989 6.50421 2.625 7 2.625C7.49579 2.625 7.875 2.99888 7.875 3.48771V7.88729C7.875 8.3763 7.49579 8.75 7 8.75Z' fill='%23FAF8F4' /%3E%3Cpath d='M6.97194 11.375C6.74608 11.375 6.52022 11.2879 6.37902 11.1136C6.20967 10.9394 6.125 10.736 6.125 10.5036C6.125 10.4455 6.125 10.3875 6.15316 10.3294C6.15316 10.2712 6.18132 10.2133 6.20967 10.1551C6.23783 10.097 6.26619 10.068 6.29435 10.0099C6.3225 9.95174 6.35086 9.92277 6.40718 9.86462C6.71771 9.54513 7.28225 9.54513 7.59282 9.86462C7.62098 9.89359 7.6775 9.95174 7.70565 10.0099C7.73381 10.068 7.76217 10.097 7.79033 10.1551C7.81849 10.2133 7.81849 10.2712 7.84684 10.3294C7.84684 10.3875 7.875 10.4455 7.875 10.5036C7.875 10.736 7.79033 10.9684 7.62098 11.1136C7.42367 11.2879 7.19781 11.375 6.97195 11.375H6.97194Z' fill='%23FAF8F4' /%3E%3C/svg%3E%0A"); - } + &:focus:invalid, + &.invalid:focus { + border-color: $justfix-black; + } + } - &.invalid { - color: #ba4300; + button { + display: flex; + align-items: center; + justify-content: center; + color: $justfix-white; + background-color: $justfix-black; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + width: 2.5rem; + } } - &.valid { - color: $justfix-green; + .password-input-rules { + display: flex; + flex-direction: column; } - &.valid::before { - content: url("data:image/svg+xml,%3Csvg width='14' height='14' viewBox='0 0 17 17' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1.16666 7.97664L5.83332 12.6433L15.8333 2.64331' stroke='%231AA551' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E "); + .password-input-rule { + @include body-standard; + + font-family: $wow-font !important; + font-size: 0.8125rem !important; + // font-weight: 500 !important; + line-height: 120%; + text-transform: none !important; + color: $justfix-black; + text-align: left; + + &::before { + content: "•"; + display: inline-block; + margin-right: 0.31rem; + vertical-align: top; + } + + // encoded SVG from https://yoksel.github.io/url-encoder/ + &.invalid:before { + content: url("data:image/svg+xml, %3Csvg width='14' height='14' viewBox='0 0 14 14' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M7 14C3.14 14 0 10.8599 0 7C0 3.14007 3.14007 0 7 0C10.8599 0 14 3.14007 14 7C14 10.84 10.8599 14 7 14Z' fill='%23FF813A' /%3E%3Cpath d='M7 8.75C6.50422 8.75 6.125 8.37612 6.125 7.88729V3.48771C6.125 2.9989 6.50421 2.625 7 2.625C7.49579 2.625 7.875 2.99888 7.875 3.48771V7.88729C7.875 8.3763 7.49579 8.75 7 8.75Z' fill='%23FAF8F4' /%3E%3Cpath d='M6.97194 11.375C6.74608 11.375 6.52022 11.2879 6.37902 11.1136C6.20967 10.9394 6.125 10.736 6.125 10.5036C6.125 10.4455 6.125 10.3875 6.15316 10.3294C6.15316 10.2712 6.18132 10.2133 6.20967 10.1551C6.23783 10.097 6.26619 10.068 6.29435 10.0099C6.3225 9.95174 6.35086 9.92277 6.40718 9.86462C6.71771 9.54513 7.28225 9.54513 7.59282 9.86462C7.62098 9.89359 7.6775 9.95174 7.70565 10.0099C7.73381 10.068 7.76217 10.097 7.79033 10.1551C7.81849 10.2133 7.81849 10.2712 7.84684 10.3294C7.84684 10.3875 7.875 10.4455 7.875 10.5036C7.875 10.736 7.79033 10.9684 7.62098 11.1136C7.42367 11.2879 7.19781 11.375 6.97195 11.375H6.97194Z' fill='%23FAF8F4' /%3E%3C/svg%3E%0A"); + } + + &.invalid { + color: #ba4300; + } + + &.valid { + color: $justfix-green; + } + + &.valid::before { + content: url("data:image/svg+xml,%3Csvg width='14' height='14' viewBox='0 0 17 17' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1.16666 7.97664L5.83332 12.6433L15.8333 2.64331' stroke='%231AA551' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E "); + } } } diff --git a/client/src/styles/UserTypeInput.scss b/client/src/styles/UserTypeInput.scss new file mode 100644 index 000000000..29f033b4f --- /dev/null +++ b/client/src/styles/UserTypeInput.scss @@ -0,0 +1,79 @@ +@import "_vars.scss"; +@import "_typography.scss"; + +.user-type-container { + .user-type-radio-group { + display: grid; + grid-template-columns: 1.25rem auto; + gap: 0.63rem 0.5rem; + align-items: center; + + label { + margin: 0; + } + + input[type="radio"] { + /* Remove most all native input styles */ + appearance: none; + /* For iOS < 15 */ + background-color: $justfix-white; + /* Not removed via appearance */ + margin: 0; + + font: inherit; + color: $justfix-black; + width: 1.25rem; + height: 1.25rem; + border: 0.15em solid $justfix-black; + border-radius: 50%; + transform: translateY(-0.075em); + + display: grid; + place-content: center; + + &::before { + content: ""; + width: 0.75rem; + height: 0.75rem; + border-radius: 50%; + transform: scale(0); + transition: 120ms transform ease-in-out; + box-shadow: inset 1em 1em $justfix-black; + /* Windows High Contrast Mode */ + background-color: CanvasText; + } + + &:checked::before { + transform: scale(1); + } + } + } + + .user-type-input-text-group { + display: flex; + flex-direction: column; + margin: 1.5rem 1.75rem 0 1.75rem; + + input { + &.invalid { + border-color: $justfix-orange; + } + + &.invalid:focus { + border-color: $justfix-black; + } + } + } + + #input-field-error { + display: flex; + align-items: center; + font-size: 0.8125rem; + color: #ba4300; + margin-bottom: 0.31rem; + + svg { + margin-right: 0.47rem; + } + } +} diff --git a/client/src/styles/_button.scss b/client/src/styles/_button.scss index 993e4b637..52e995fd8 100644 --- a/client/src/styles/_button.scss +++ b/client/src/styles/_button.scss @@ -170,6 +170,7 @@ white-space: normal; word-wrap: break-word; text-decoration: none; + text-transform: unset; transition: all 0.1s linear; transform: translateX(0rem); diff --git a/client/src/styles/_datatable.scss b/client/src/styles/_datatable.scss index 017ae288c..625e91f23 100644 --- a/client/src/styles/_datatable.scss +++ b/client/src/styles/_datatable.scss @@ -45,13 +45,13 @@ } @include for-phone-only() { - font-size: 0.81rem; + font-size: 0.8125rem; } > label { display: block; line-height: 1.1; - font-size: 0.81rem; + font-size: 0.8125rem; padding: 1px 0; // background-color: $background; border-bottom: 1px solid $gray-dark; @@ -75,11 +75,11 @@ .table-content { margin: 0.63rem 0.47rem; - line-height: 0.81rem; + line-height: 0.8125rem; } .table-small-font { - font-size: 0.81rem; + font-size: 0.8125rem; } } } diff --git a/client/src/styles/_input.scss b/client/src/styles/_input.scss index b0c9fe9e4..b79b35798 100644 --- a/client/src/styles/_input.scss +++ b/client/src/styles/_input.scss @@ -3,7 +3,6 @@ @mixin input() { display: inline-block; border: 1px solid $justfix-black; - background: $justfix-white; padding: 0.63rem; font-size: inherit; border-radius: 4px; diff --git a/client/src/styles/_tabs.scss b/client/src/styles/_tabs.scss index c8224b0a9..d3f1e7f62 100644 --- a/client/src/styles/_tabs.scss +++ b/client/src/styles/_tabs.scss @@ -157,7 +157,7 @@ } a { - font-size: 0.81rem; + font-size: 0.8125rem; line-height: 1.5; border-bottom: none; diff --git a/client/src/util/helpers.ts b/client/src/util/helpers.ts index 896bdc0f2..be38f0cd5 100644 --- a/client/src/util/helpers.ts +++ b/client/src/util/helpers.ts @@ -3,7 +3,7 @@ import { AddressRecord, HpdContactAddress, SearchAddressWithoutBbl } from "compo import { reportError } from "error-reporting"; import { t } from "@lingui/macro"; import { I18n, MessageDescriptor } from "@lingui/core"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, ChangeEvent } from "react"; import _ from "lodash"; const hpdComplaintTypeTranslations = new Map([ @@ -132,6 +132,27 @@ export function searchAddrsAreEqual( ); } +// https://www.codevertiser.com/reusable-input-component-react/ +export const useInput = (initialValue: string) => { + const [value, setValue] = useState(initialValue); + const [error, setError] = useState(false); + const [showError, setShowError] = useState(false); + + const handleChange = (e: ChangeEvent) => { + setValue(e.target.value); + }; + + return { + value, + error, + showError, + setValue, + setError, + setShowError, + onChange: handleChange, + }; +}; + const helpers = { // filter repeated values in rbas and owners // uses Set which enforces uniqueness diff --git a/jfauthprovider/views.py b/jfauthprovider/views.py index 975764821..ddf5f0847 100644 --- a/jfauthprovider/views.py +++ b/jfauthprovider/views.py @@ -77,6 +77,7 @@ def register(request): "grant_type": "password", "username": request.POST.get("username"), "password": request.POST.get("password"), + "user_type": request.POST.get("user_type"), "origin": request.headers["Origin"], }