login view on AccountModal working

This commit is contained in:
Victor committed 2024-01-16 15:17:03 -08:00
1 parent a3d2b1244a
commit bb90f4594c
14 files changed
+147 -248

No files matched your search

@@ -0,0 +1,17 @@
import MakeRequest from "../MakeRequest";
export interface CreateSessionRequest {
username_or_email: string,
password: string,
}
export interface CreateSessionResponse {
success: boolean,
error_type?: string,
error_message?: string,
}
export const CreateSession = MakeRequest<string, CreateSessionRequest, CreateSessionResponse,{}>({
method: "POST",
routingFunction: () => "/v1/login",
});
+1
View File
@@ -842,6 +842,7 @@ class App extends React.Component<Props, State> {
<SessionProvider
{...{
querySession: this.querySession,
querySubscriptions: this.querySessionSubscriptions,
sessionWrapper: this.state.sessionWrapper,
sessionFetched: this.state.sessionFetched
}}
@@ -4,5 +4,5 @@
justify-content: space-between;
margin-bottom: .5rem;
span { margin: 0;}
.label-error { margin: 0; margin-top: 0; }
}
@@ -12,7 +12,7 @@ export default function Label({ invalidReason, label, required }: Props) {
{ label ? <label {...{ className: `{required ? " required" : ""}` }}>
{ label }
</label> : null }
{ invalidReason ? <span {...{ className: "form-text red is-danger" }}>
{ invalidReason ? <span {...{ className: "label-error form-text red is-danger" }}>
{ invalidReason }
</span> : null }
</div>;
@@ -48,12 +48,19 @@
transition: opacity .25s ease-in-out;
}
.error-message {
color: $primary;
margin-bottom: 1rem;
text-align: center;
}
}
.modal-spinner {
height: 300px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
@@ -2,6 +2,7 @@ import React, { useState } from "react";
import { a, useTransition } from "@react-spring/web";
import { FetchStatus } from "@storyteller/components/src/api/_common/SharedFetchTypes";
import { Spinner } from "components/common";
import useLogin from "./useLogin";
import useSignup from "./useSignup";
import LoginView from "./LoginView";
import SignupView from "./SignupView";
@@ -22,13 +23,26 @@ const AniMod = ({ animating, isLeaving, render: Render, style, ...rest }: AniPro
<Render {...{ ...rest, animating }} />
</a.div>;
const Loader = () => <div {...{ className: `fy-modal-page modal-spinner` }}><Spinner /></div>;
const Loader = ({ viewLogin }: { viewLogin: boolean }) => <div {...{ className: `fy-modal-page modal-spinner` }}>
<h3> {
viewLogin ? "Logging you in" : "Creating your account"
} </h3>
<Spinner />
</div>;
export default function LoginModal({ handleClose }: { handleClose: any }) {
const [status,statusSet] = useState(FetchStatus.paused);
const [viewLogin,viewLoginSet] = useState(false);
const [animating,animatingSet] = useState(false);
const { setProps, signup } = useSignup({
const { errorType, setProps: loginProps, login } = useLogin({
onSuccess: () => {
Analytics.accountLoginSuccess();
handleClose();
},
status,
statusSet
});
const { setProps: signupProps, signup } = useSignup({
onSuccess: () => {
Analytics.accountSignupAttempt();
handleClose();
@@ -54,13 +68,13 @@ export default function LoginModal({ handleClose }: { handleClose: any }) {
{
transitions((style: any, i: number, state: any) => {
let isLeaving = state.phase === "leave";
let sharedProps = { animating, handleClose, isLeaving, style, viewLogin, viewLoginSet };
switch(i) {
case 0: return <AniMod {...{ animating, handleClose, isLeaving, render: SignupView, setProps, signup, style, viewLoginSet }}/>;
case 1: return <AniMod {...{ animating, handleClose, isLeaving, render: LoginView, setProps, signup, style, viewLoginSet }}/>;
case 2: return <AniMod {...{ animating, handleClose, isLeaving, render: Loader, setProps, signup, style, viewLoginSet }}/>;
case 0: return <AniMod {...{ render: SignupView, signupProps, signup, ...sharedProps }}/>;
case 1: return <AniMod {...{ errorType, login, loginProps, render: LoginView, ...sharedProps }}/>;
case 2: return <AniMod {...{ render: Loader, ...sharedProps }}/>;
}
})
}
@@ -1,16 +1,27 @@
import React from 'react';
import { TempInput } from "components/common";
import { faUser, faEnvelope, faKey } from "@fortawesome/free-solid-svg-icons";
import { faUser, faKey } from "@fortawesome/free-solid-svg-icons";
enum ErrorTypes {
InvalidCredentials = "abc",
ServerError = "abc"
}
interface Props {
animating: boolean,
errorType?: string,
handleClose: (x:any) => any,
setProps: (x:any) => any,
signup: (x:any) => any,
login: (x:any) => any,
loginProps: (x:any) => any,
viewLoginSet: (x:any) => void
}
export default function LoginView({ animating, handleClose, setProps, signup, viewLoginSet }: Props) {
export default function LoginView({ animating, errorType = "", handleClose, login, loginProps, viewLoginSet }: Props) {
const errorStrings = [
"Could not login, check credentials and try again.",
"A sever error occured. Try again."
];
return <div {...{ className: `fy-modal-page${ animating ? " animating-modal-page" : ""}` }}>
<header>
<div {...{ className: "login-modal-title-row" }}>
@@ -25,8 +36,9 @@ export default function LoginView({ animating, handleClose, setProps, signup, vi
}}>Signup instead</span>
</div>
</header>
<TempInput {...{ icon: faUser, label: "Username or email", placeholder: "Username", ...setProps("username") }}/>
<TempInput {...{ icon: faKey, label: "Password", placeholder: "Enter your password", type: "password", ...setProps("password") }}/>
<button {...{ className: "btn btn-primary w-100 mt-4", onClick: signup }}>Login</button>
{ errorType ? <p {...{ className: "error-message" }}>{ errorStrings[Object.keys(ErrorTypes).indexOf(errorType)] }</p> : null }
<TempInput {...{ icon: faUser, label: "Username or email", placeholder: "Username", ...loginProps("username") }}/>
<TempInput {...{ icon: faKey, label: "Password", placeholder: "Enter your password", type: "password", ...loginProps("password") }}/>
<button {...{ className: "btn btn-primary w-100 mt-4", disabled: animating, onClick: login }}>Login</button>
</div>;
};
@@ -5,12 +5,12 @@ import { faUser, faEnvelope, faKey } from "@fortawesome/free-solid-svg-icons";
interface Props {
animating: boolean,
handleClose: (x:any) => any,
setProps: (x:any) => any,
signupProps: (x:any) => any,
signup: (x:any) => any,
viewLoginSet: (x:any) => void
}
export default function LoginView({ animating, handleClose, setProps, signup, viewLoginSet }: Props) {
export default function LoginView({ animating, handleClose, signupProps, signup, viewLoginSet }: Props) {
return <div {...{ className: `fy-modal-page${ animating ? " animating-modal-page" : ""}` }}>
<header>
<div {...{ className: "login-modal-title-row" }}>
@@ -22,10 +22,10 @@ export default function LoginView({ animating, handleClose, setProps, signup, vi
<span {...{ className: "login-modal-login-link", onClick: () => { if (!animating) viewLoginSet(true); } }}>Or login instead</span>
</div>
</header>
<TempInput {...{ icon: faUser, label: "Username", placeholder: "Username", ...setProps("username") }}/>
<TempInput {...{ icon: faEnvelope, label: "Email", placeholder: "Email", ...setProps("email") }}/>
<TempInput {...{ icon: faKey, label: "Password", placeholder: "Enter a new password", type: "password", ...setProps("password") }}/>
<TempInput {...{ icon: faKey, label: "Confirm password", placeholder: "Confirm password", type: "password", ...setProps("passwordConfirm") }}/>
<button {...{ className: "btn btn-primary w-100 mt-4", onClick: signup }}>Sign up</button>
<TempInput {...{ icon: faUser, label: "Username", placeholder: "Username", ...signupProps("username") }}/>
<TempInput {...{ icon: faEnvelope, label: "Email", placeholder: "Email", ...signupProps("email") }}/>
<TempInput {...{ icon: faKey, label: "Password", placeholder: "Enter a new password", type: "password", ...signupProps("password") }}/>
<TempInput {...{ icon: faKey, label: "Confirm password", placeholder: "Confirm password", type: "password", ...signupProps("passwordConfirm") }}/>
<button {...{ className: "btn btn-primary w-100 mt-4", disabled: animating, onClick: signup }}>Sign up</button>
</div>;
};
@@ -1,143 +1,7 @@
import { useState } from "react";
import { useChanger, useSession } from "hooks";
import { FetchStatus } from "@storyteller/components/src/api/_common/SharedFetchTypes";
import { CreateAccount, CreateAccountResponse } from "@storyteller/components/src/api/account/CreateAccount";
import { InputValidation, ValidatorCallbacks } from "common/InputValidation";
enum EmailReasons {
EmailTaken = "emailTaken",
Invalid = "invalid",
TooShort = "tooShort",
BadInput = "badInput"
}
enum PasswordReasons {
TooShort = "tooShort"
}
enum PasswordConfirmReasons {
PasswordMismatch = "passwordMismatch",
TooShort = "tooShort"
}
enum UsernameReasons {
ContainsSlurs = "containsSlurs",
InvalidCharacters = "invalidCharacters",
IsReserved = "isReserved",
IsTaken = "isTaken",
TooLong = "tooLong",
TooShort = "tooShort"
}
const emailErrors = {
emailTaken: "Email is taken",
invalid: "Email is invalid",
tooShort: "Email is too short",
badInput: "Bad email input, try another"
};
const passwordErrors = {
tooShort: "Password is too short",
};
const passwordConfirmErrors = {
passwordMismatch: "Passwords do not match",
tooShort: "Password is too short"
};
const usernameErrors = {
containsSlurs: "Username contains slurs",
invalidCharacters: "Username has invalid characters",
isReserved: "Username is reserved",
isTaken: "Username is taken",
tooLong: "Username is too long",
tooShort: "Username is too short"
};
const emailValidator = ({ inputValue }: ValidatorCallbacks) => {
let reason = "";
let validation = InputValidation.Neutral;
if (inputValue.length > 1) {
if (inputValue.length < 3) {
validation = InputValidation.Invalid;
reason = EmailReasons.TooShort;
} else if (!inputValue.includes("@")) {
validation = InputValidation.Invalid;
reason = EmailReasons.Invalid;
} else {
validation = InputValidation.Valid;
}
}
return { value: inputValue, reason, validation };
};
const passwordConfirmValidator = ({ inputValue, state }: ValidatorCallbacks) => {
const password = state.password;
let validation = InputValidation.Neutral;
let reason = "";
if (inputValue.length > 1) {
if (inputValue !== password.value) {
validation = InputValidation.Invalid;
reason = PasswordConfirmReasons.PasswordMismatch;
} else {
validation = InputValidation.Valid;
reason = "";
}
}
return { value: inputValue, reason, validation };
};
const passwordValidator = ({ inputValue, state }: ValidatorCallbacks) => {
const passwordConfirm = state.passwordConfirm;
let validation = InputValidation.Neutral;
let reason = "";
let confirmValidation = InputValidation.Neutral;
let confirmReason = "";
if (inputValue.length > 1) {
if (inputValue.length < 6) {
validation = InputValidation.Invalid;
reason = PasswordReasons.TooShort;
} else {
validation = InputValidation.Valid;
}
if (inputValue !== passwordConfirm.value) {
confirmValidation = InputValidation.Invalid;
confirmReason = PasswordConfirmReasons.PasswordMismatch
} else {
confirmValidation = InputValidation.Valid;
confirmReason = "";
}
}
const additional = { passwordConfirm: { ...passwordConfirm, reason: confirmReason, validation: confirmValidation } }
return { additional, value: inputValue, reason, validation };
};
const usernameValidator = ({ inputValue }: ValidatorCallbacks) => {
let reason = "";
let validation = InputValidation.Neutral;
if (inputValue.length > 1) {
if (inputValue.length < 3) {
validation = InputValidation.Invalid;
reason = UsernameReasons.TooShort
} else if (inputValue.length > 15) {
validation = InputValidation.Invalid;
reason = UsernameReasons.TooLong
} else {
validation = InputValidation.Valid;
}
}
return { value: inputValue, reason, validation };
};
import { CreateSession, CreateSessionResponse } from "@storyteller/components/src/api/account/CreateSession";
interface Props {
onSuccess?: (x?:any) => any,
@@ -146,71 +10,34 @@ interface Props {
}
export default function useLogin({ onSuccess, status, statusSet }: Props) {
const { querySession } = useSession();
const [errorType,errorTypeSet] = useState("");
const { querySession, querySubscriptions } = useSession();
const { allAreValid, setProps, state, update } = useChanger({
errorStrings: {
email: emailErrors,
password: passwordErrors,
passwordConfirm: passwordConfirmErrors,
username: usernameErrors,
},
state: {
email: "",
password: "",
passwordConfirm: "",
username: ""
},
validators: {
email: emailValidator,
password: passwordValidator,
passwordConfirm: passwordConfirmValidator,
username: usernameValidator
}
password: { value: "" },
usernameOrEmail: { value: "" }
});
const signup = () => {
const login = () => {
console.log("🐊",);
errorTypeSet("");
statusSet(FetchStatus.in_progress);
// if (allAreValid()) {
CreateAccount("",{
username: state.username.value,
email_address: state.email.value,
CreateSession("",{
username_or_email: state.usernameOrEmail.value,
password: state.password.value,
password_confirmation: state.passwordConfirm.value,
}).then((res: CreateAccountResponse) => {
}).then((res: CreateSessionResponse) => {
console.log("🦋",res);
// let updates = {}
if (res && res.error_fields && res.error_type) {
statusSet(FetchStatus.error);
if (res.error_fields.email_address) {
console.log("🍟",res.error_type);
switch (res.error_type) {
case "BadInput": return update({ name: "email", reason: EmailReasons.BadInput, validation: InputValidation.Invalid });
case "EmailTaken": return update({ name: "email", reason: EmailReasons.EmailTaken, validation: InputValidation.Invalid });
}
return;
} else if (res.error_fields.password) {
switch (res.error_type) {
case "TooShort": update({ name: "password", reason: PasswordReasons.TooShort, validation: InputValidation.Invalid });
}
} else if (res.error_fields?.username) {
let updateUsername = (reason: UsernameReasons) => update({ reason, name: "username", validation: InputValidation.Invalid });
switch (res.error_fields.username) {
case "invalid username characters": return updateUsername(UsernameReasons.InvalidCharacters);
case "username is too long": return updateUsername(UsernameReasons.TooLong);
case "username is taken": return updateUsername(UsernameReasons.IsTaken);
case "username is reserved": return updateUsername(UsernameReasons.IsReserved);
case "username contains slurs": return updateUsername(UsernameReasons.ContainsSlurs);
}
}
if (!res.success && res.error_type) {
statusSet(FetchStatus.error);
errorTypeSet(res.error_type);
} else if (res.success) {
statusSet(FetchStatus.success);
querySession();
querySubscriptions();
if (onSuccess) onSuccess(res);
}
});
// }
};
};
return { allAreValid, setProps, signup, state, update };
return { allAreValid, errorType, errorTypeSet, login, setProps, state, update };
};
@@ -145,27 +145,29 @@ interface Props {
statusSet: (x: FetchStatus) => void,
}
export default function useLogin({ onSuccess, status, statusSet }: Props) {
export default function useSignup({ onSuccess, status, statusSet }: Props) {
const { querySession } = useSession();
const { allAreValid, setProps, state, update } = useChanger({
errorStrings: {
email: emailErrors,
password: passwordErrors,
passwordConfirm: passwordConfirmErrors,
username: usernameErrors,
email: {
errorText: emailErrors,
validator: emailValidator,
value: ""
},
state: {
email: "",
password: "",
passwordConfirm: "",
username: ""
password: {
errorText: passwordErrors,
validator: passwordValidator,
value: ""
},
passwordConfirm: {
errorText: passwordConfirmErrors,
validator: passwordConfirmValidator,
value: ""
},
username: {
errorText: usernameErrors,
validator: usernameValidator,
value: ""
},
validators: {
email: emailValidator,
password: passwordValidator,
passwordConfirm: passwordConfirmValidator,
username: usernameValidator
}
});
const signup = () => {
@@ -4,12 +4,13 @@ import { SessionContext } from 'context';
interface Props {
children?: any;
querySession: any;
querySubscriptions: any;
sessionFetched: boolean;
sessionWrapper?: any;
}
export default function Session({ children, querySession, sessionFetched, sessionWrapper }: Props) {
return <SessionContext.Provider {...{ value: { querySession, sessionFetched, sessionWrapper } }}>
export default function Session({ children, querySession, querySubscriptions, sessionFetched, sessionWrapper }: Props) {
return <SessionContext.Provider {...{ value: { querySession, querySubscriptions, sessionFetched, sessionWrapper } }}>
{ children }
</SessionContext.Provider>
};
@@ -2,6 +2,7 @@ import { createContext } from "react";
interface SessionContextType {
querySession?: any,
querySubscriptions?: any,
sessionFetched: boolean,
sessionWrapper?: any
}
@@ -1,27 +1,36 @@
import { useState } from "react";
import { InputValidation, InputStateLibrary, ValidatorCallbacks } from "common/InputValidation";
interface Props {
errorStrings: { [key:string]: { [error:string]: string } },
state: { [key:string]: any },
validators: { [key: string]: (value:any) => any }
// interface Props {
// errorStrings: { [key:string]: { [error:string]: string } },
// state: { [key:string]: any },
// validators: { [key: string]: (value:any) => any }
// }
interface InputInput {
errorText?: { [key: string]: string },
validator?: (value: ValidatorCallbacks) => ValidatorOutput,
value: any,
}
interface ValidatorOutput { additional: any, value: any, reason: string, validation: any }
interface Props {
[key:string]: InputInput
}
interface ValidatorOutput { additional?: any, value: any, reason: string, validation: any }
const noValidation = ({ inputValue }: ValidatorCallbacks) => ({ value: inputValue, reason: "", validation: InputValidation.Neutral });
export default function useChanger({ errorStrings, state: inputState, validators }: Props) {
const baseValidation = Object.keys(inputState).reduce((obj,key) => ({ ...obj, [key]: {
export default function useChanger(input: Props) {
const baseValidation = Object.keys(input).reduce((obj,key) => ({ ...obj, [key]: {
reason: "",
validation: InputValidation.Neutral,
value: inputState[key]
value: input[key].value
} }),{})
const [state,stateSet] = useState<InputStateLibrary>(baseValidation);
const changer = (name: string) => ({ target }: { target: { value: any } }) => {
// const setter = state[name][1];
const validator = validators[name] ? validators[name] : noValidation;
const validator = input[name]?.validator || noValidation;
const { additional, value, reason, validation, } : ValidatorOutput = validator({ inputValue: target.value, name, state });
const updateValidations = ( initial: InputStateLibrary ) => ({
...initial,
@@ -36,12 +45,19 @@ export default function useChanger({ errorStrings, state: inputState, validators
const allAreValid = () => !Object.values(state).find((item,i) =>
((item.validation === InputValidation.Neutral) || (item.validation === InputValidation.Invalid)) );
const setProps = (name: string) => ({
name,
value: state[name].value,
invalidReason: errorStrings[name][state[name].reason] || "",
onChange: changer(name)
});
const setProps = (name: string) => {
const config = input[name];
const current = state[name];
if (config && current) {
console.log("🍇",config.errorText,current);
return {
name,
value: current.value,
invalidReason: config.errorText ? config.errorText[current.reason] : "",
onChange: changer(name)
}
}
};
const update = ({ name, reason, validation }: { name: string, reason: string, validation: number }) =>
stateSet((lib: InputStateLibrary) => ({
@@ -2,12 +2,13 @@ import { useContext } from 'react';
import { SessionContext } from 'context';
export default function useInferenceJobs() {
const { querySession, sessionFetched, sessionWrapper } = useContext(SessionContext);
const { querySession, querySubscriptions, sessionFetched, sessionWrapper } = useContext(SessionContext);
const { logged_in: loggedIn, user } = sessionWrapper?.sessionStateResponse || { logged_in: false };
return {
loggedIn,
querySession,
querySubscriptions,
sessionFetched,
user
};