#1 feature/forgot-password-flow

已合并
kj 4 年前 将 2 次代码提交从 feature/forgot-password-flow合并至 master
  1. +4
    -0
      src/App.tsx
  2. +143
    -0
      src/commonStyles/loginFlow/LoginStyles.module.scss
  3. +14
    -1
      src/components/input/InputWidget.module.scss
  4. +6
    -2
      src/components/input/InputWidget.tsx
  5. +54
    -0
      src/pages/forgotPassword/EnterNewPassword.module.scss
  6. +58
    -0
      src/pages/forgotPassword/EnterOTP.module.scss
  7. +49
    -0
      src/pages/forgotPassword/ForgotPassword.module.scss
  8. +56
    -0
      src/pages/forgotPassword/enterNewPassword.tsx
  9. +72
    -0
      src/pages/forgotPassword/enterOTP.tsx
  10. +41
    -0
      src/pages/forgotPassword/forgotPassword.tsx
  11. +22
    -0
      src/pages/forgotPassword/forgotPasswordIndex.tsx
  12. +2
    -58
      src/pages/login/Login.module.scss
  13. +4
    -3
      src/pages/login/Login.tsx
  14. +4
    -7
      src/pages/signup/AdditionalQuestions.module.scss
  15. +3
    -2
      src/pages/signup/Signup.module.scss
  16. +9
    -3
      src/theme/variables.css

+ 4
- 0
src/App.tsx 查看文件

@@ -4,6 +4,7 @@ import { IonReactRouter } from '@ionic/react-router';
import WelcomePage from './pages/onboarding/Welcome'; import WelcomePage from './pages/onboarding/Welcome';
import LoginPage from './pages/login/Login'; import LoginPage from './pages/login/Login';
import SignupPage from './pages/signup/Signup'; import SignupPage from './pages/signup/Signup';
import ForgotPasswordIndex from './pages/forgotPassword/forgotPasswordIndex';


/* Core CSS required for Ionic components to work properly */ /* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css'; import '@ionic/react/css/core.css';
@@ -41,6 +42,9 @@ const App: React.FC = () => (
<Route exact path="/"> <Route exact path="/">
<Redirect to="/welcome" /> <Redirect to="/welcome" />
</Route> </Route>
<Route exact path="/forgotPassword">
<ForgotPasswordIndex />
</Route>
</IonRouterOutlet> </IonRouterOutlet>
</IonReactRouter> </IonReactRouter>
</IonApp> </IonApp>


+ 143
- 0
src/commonStyles/loginFlow/LoginStyles.module.scss 查看文件

@@ -0,0 +1,143 @@
.upfold {
background-color: var(--charcoal);
height: auto;
transform: translateY(-50vh);
width: 100%;
border-bottom-right-radius: 30px;
border-bottom-left-radius: 30px;
position: relative;
z-index: 1;
box-shadow: 0px 0px 10px 5px var(--black-rock);
animation: riseDown 1s forwards;
display: flex;
align-items: center;
justify-content: center;

.container {
padding: 20px 5%;
text-align: center;
}

h2 {
font-size: 26px;
color: var(--white);
margin: 10px 0;
}

p {
margin: 10px 0;
font-size: 14px;
color: var(--grey-rock);
}

figure {
display: block;
width: 100%;
margin: 10px 0;
animation: fadeIn 1s forwards;
opacity: 0;
transform: translateY(10vh);
}

img {
margin: 0 auto;
width: 50%;
display: block;
}

@keyframes riseDown {
from {
transform: translateY(-50vh);
}
to {
transform: translateY(0vh);
}
}
}

@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10vh);
}

to {
opacity: 1;
transform: translateY(0vh);
}
}

.margTopBtm20 {
margin: 20px 0;
}

.linkBtn {
&.greyLink {
color: var(--ash-dust);
}

&.whiteLink {
color: var(--ion-color-primary-contrast);
}

&.shamrockLink {
color: var(--shamrock);
}

&.shamrockBG {
background-color: var(--shamrock);
padding: 4px 6px;
border-radius: 10px;
}

&.fullWidth {
display: flex;
justify-content: center;
width: 100%;
}
}

.successToast {
--background: transparent;
--box-shadow: none;

.toast-wrapper {
background-color: red;
}

&::part(header) {
font-size: 16px;
font-weight: bold;
background: var(--white);
width: 175px;
padding: 10px 0;
height: auto;
text-align: center;
border-radius: 20px;
color: var(--shamrock);
position: relative;
top: 20px;
left: 20px;
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.25);

&::before {
content: "\2713";
display: inline-block;
width: 20px;
height: 20px;
border-radius: 50%;
border: 4px solid var(--shamrock);
position: relative;
left: -20px;
}
}

&::part(message) {
padding: 30px 15px 12px;
background: var(--shamrock);
color: var(--white);
font-size: 12px;
border-radius: 20px;
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.25);
}
}

+ 14
- 1
src/components/input/InputWidget.module.scss 查看文件

@@ -1,5 +1,5 @@
.inputHolder { .inputHolder {
background-color: white;
background-color: var(--white);
box-shadow: 0px 0px 5px inset var(--grey-rock); box-shadow: 0px 0px 5px inset var(--grey-rock);
border-radius: 30px; border-radius: 30px;
display: flex; display: flex;
@@ -32,3 +32,16 @@
font-size: 16px; font-size: 16px;
} }
} }

.rounded {
width: 60px;
height: 60px;
border-radius: 50%;

input {
width: 100%;
text-align: center;
padding: 0;
font-size: 18px;
}
}

+ 6
- 2
src/components/input/InputWidget.tsx 查看文件

@@ -6,14 +6,17 @@ import styles from './InputWidget.module.scss';
type Props = { type Props = {
icon?: string, icon?: string,
placeholder?: string, placeholder?: string,
type: 'TEXT' | 'PASSWORD' | 'PHONE';
type: 'TEXT' | 'PASSWORD' | 'PHONE' | 'NUMBER';
hideEye?: boolean hideEye?: boolean
displayType?: string
autoTabHandler?: any
maxlength?: number
}; };


const InputWidget: React.FC<Props> = (props) => { const InputWidget: React.FC<Props> = (props) => {
let [showPassword, toggleEye] = useState(false); let [showPassword, toggleEye] = useState(false);


return (<section className={styles.inputHolder}>
return (<section className={`${styles.inputHolder} ${props.displayType? styles.rounded : ''}`} >
{ props.icon && <IonIcon className={styles.leftIcon} icon={props.icon}></IonIcon> } { props.icon && <IonIcon className={styles.leftIcon} icon={props.icon}></IonIcon> }
{ props.type === 'TEXT' && <input type='text' placeholder={props.placeholder} /> } { props.type === 'TEXT' && <input type='text' placeholder={props.placeholder} /> }
@@ -27,6 +30,7 @@ const InputWidget: React.FC<Props> = (props) => {
{ !showPassword && <IonIcon icon={eyeOutline}></IonIcon> } { !showPassword && <IonIcon icon={eyeOutline}></IonIcon> }
</IonButton> </IonButton>
} }
{ props.type === 'NUMBER' && <input type="number" maxLength={props.maxlength} placeholder={props.placeholder} pattern="[0-9]?" onKeyUp={props.autoTabHandler} /> }
</section>); </section>);
}; };




+ 54
- 0
src/pages/forgotPassword/EnterNewPassword.module.scss 查看文件

@@ -0,0 +1,54 @@
.inputForm {
width: 75%;
margin: 40px auto 0;
opacity: 0;
transform: translateY(10vh);
position: relative;
animation: fadeIn 1s forwards;

.input {
margin: 20px 0;
}

.otpInput {
display: flex;
justify-content: space-between;
}

.actionButtonsHolder {
width: 60%;
margin: 20px auto;

.actionButton {
height: 50px;
text-transform: none;
font-size: 16px;
--border-radius: 30px;
--border-color: var(--shamrock);
font-weight: 500;

&:nth-child(1) {
--background: var(--shamrock);
}
}
}

.info {
font-size: 14px;
text-align: center;
color: var(--ash);
margin: 40px 0;
}
}

@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10vh);
}

to {
opacity: 1;
transform: translateY(0vh);
}
}

+ 58
- 0
src/pages/forgotPassword/EnterOTP.module.scss 查看文件

@@ -0,0 +1,58 @@
.inputForm {
width: 75%;
margin: 40px auto 0;
opacity: 0;
transform: translateY(10vh);
position: relative;
animation: fadeIn 1s forwards;

.input {
margin: 20px 0;
}

.otpInput {
display: flex;
justify-content: space-between;
}

.actionButtonsHolder {
width: 60%;
margin: 20px auto;

.actionButton {
height: 50px;
text-transform: none;
font-size: 16px;
--border-radius: 30px;
--border-color: var(--shamrock);
font-weight: 500;

&:nth-child(1) {
--background: var(--shamrock);
}

&.disabled {
--background: var(--ash-dust);
}
}
}

.info {
font-size: 14px;
text-align: center;
color: var(--grey-rock);
margin: 40px 0;
}
}

@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10vh);
}

to {
opacity: 1;
transform: translateY(0vh);
}
}

+ 49
- 0
src/pages/forgotPassword/ForgotPassword.module.scss 查看文件

@@ -0,0 +1,49 @@
.inputForm {
width: 80%;
margin: 40px auto 0;
opacity: 0;
transform: translateY(10vh);
position: relative;
animation: fadeIn 1s forwards;

.input {
margin: 20px 0;
}

.actionButtonsHolder {
width: 60%;
margin: 20px auto;

.actionButton {
height: 50px;
text-transform: none;
font-size: 16px;
--border-radius: 30px;
--border-color: var(--shamrock);
font-weight: 500;

&:nth-child(1) {
--background: var(--shamrock);
}
}
}

.info {
font-size: 14px;
text-align: center;
color: var(--grey-rock);
margin: 40px 0;
}
}

@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10vh);
}

to {
opacity: 1;
transform: translateY(0vh);
}
}

+ 56
- 0
src/pages/forgotPassword/enterNewPassword.tsx 查看文件

@@ -0,0 +1,56 @@
import { IonContent, IonButton, IonToast } from '@ionic/react';
import { lockOpenOutline } from 'ionicons/icons';
import React, { useState } from 'react';
import InputWidget from '../../components/input/InputWidget';
import styles from './EnterOTP.module.scss';
import loginStyles from '../../commonStyles/loginFlow/LoginStyles.module.scss';



type Props = { };

const EnterOTPView: React.FC<Props> = () => {

const [successState, setSuccessState] = useState(false);

return(
<IonContent>
<section className={loginStyles.upfold}>
<div className={loginStyles.container}>
<figure>
<img src='assets/images/welcome/upfold.svg' alt='upfold image'/>
</figure>
<h2> Enter New Password </h2>
<p>Your password must be at least 6 characters.</p>
</div>
</section>
<section className={styles.inputForm}>
<div className={styles.input}>
<InputWidget type={'PASSWORD'} icon={lockOpenOutline} placeholder={'Enter your new password'} />
</div>
<div className={styles.input}>
<InputWidget type={'PASSWORD'} icon={lockOpenOutline} placeholder={'Confirm new password'} />
</div>
<div className={styles.info}>
<p>(i) Do not enter any of the old passwords, system will reject the repeated passwords.</p>
</div>
<div className={styles.actionButtonsHolder}>
<IonButton className={`${styles.actionButton}`} expand='block' onClick={()=>setSuccessState(true)}> Change </IonButton>
<IonToast
isOpen={successState}
onDidDismiss={() => setSuccessState(false)}
header= 'Success!'
message="You have successfully changed your password. Please use your newly set password"
// duration={2000}
cssClass={loginStyles.successToast}
/>
</div>
</section>
</IonContent>

)

}

export default EnterOTPView;

+ 72
- 0
src/pages/forgotPassword/enterOTP.tsx 查看文件

@@ -0,0 +1,72 @@
import { IonContent, IonButton } from '@ionic/react';
import React, { useState } from 'react';
import InputWidget from '../../components/input/InputWidget';
import styles from './EnterOTP.module.scss';
import loginStyles from '../../commonStyles/loginFlow/LoginStyles.module.scss';



type Props = {
changeStep:any,
forgotPasswordStep:string
};

const EnterOTPView: React.FC<Props> = (props) => {

const [disableSubmitBtn, updateBtnState] = useState(true)

const autoTab = (event:any) => {
const { maxLength, value } = event.target;
if ((event.keyCode >= 48 && event.keyCode <= 57) || (event.keyCode >=96 && event.keyCode <= 105)){ /* check number keyCode */
if(value.length == maxLength){
const nextField = event.target.parentElement.nextElementSibling?.querySelector('input')
if (nextField !== null) {
nextField?.focus();
nextField?.select(); // temporary usage to restrict 1 input value; could be changed once form validations are added
}
}
}

if(disableSubmitBtn){
updateBtnState(false)
}
}
return(
<IonContent>
<section className={loginStyles.upfold}>
<div className={loginStyles.container}>
<figure>
<img src='assets/images/welcome/upfold.svg' alt='upfold image'/>
</figure>
<h2> Enter OTP </h2>
<p>Please enter the OTP that has been sent to </p>
<p>angelashelton@email.com <button className={`${loginStyles.linkBtn} ${loginStyles.whiteLink} ${loginStyles.shamrockBG}`}> change </button></p>
</div>
</section>
<section className={styles.inputForm}>
<div className={`${styles.input} ${styles.otpInput}`}>
<InputWidget autoTabHandler={autoTab} type={'NUMBER'} maxlength={1} displayType="rounded" />
<InputWidget autoTabHandler={autoTab} type={'NUMBER'} maxlength={1} displayType="rounded" />
<InputWidget autoTabHandler={autoTab} type={'NUMBER'} maxlength={1} displayType="rounded" />
<InputWidget autoTabHandler={autoTab} type={'NUMBER'} maxlength={1} displayType="rounded" />
</div>
<div className={styles.info}>
<p>Enter the 4 digit OTP which you received in your mobile</p>
</div>
<div className={styles.actionButtonsHolder}>
<IonButton className={`${styles.actionButton} ${disableSubmitBtn ? styles.disabled : ''}`} expand='block' onClick={()=>props.changeStep('NEWPASSWORD')} disabled={disableSubmitBtn}> Submit </IonButton>
<a className={`${loginStyles.linkBtn} ${loginStyles.shamrockLink} ${loginStyles.fullWidth} ${loginStyles.margTopBtm20} `} > Resend OTP </a>
</div>
</section>
</IonContent>

)

}

export default EnterOTPView;

+ 41
- 0
src/pages/forgotPassword/forgotPassword.tsx 查看文件

@@ -0,0 +1,41 @@
import { IonContent, IonPage, IonButton } from '@ionic/react';
import { mailOpenOutline } from 'ionicons/icons';
import React from 'react';
import InputWidget from '../../components/input/InputWidget';
import styles from './ForgotPassword.module.scss';
import loginStyles from '../../commonStyles/loginFlow/LoginStyles.module.scss';

type Props = {
changeStep:any,
forgotPasswordStep:string
};

const ForgotPasswordPage: React.FC<Props> = (props) => {
return (
<IonContent fullscreen>
<section className={loginStyles.upfold}>
<div className={loginStyles.container}>
<figure>
<img src='assets/images/welcome/upfold.svg' alt='upfold image'/>
</figure>
<h2> Forgot your Password? </h2>
<p> Lorem Ipsum is simply dummy text of the printing and typesetting </p>
</div>
</section>
<section className={styles.inputForm}>
<div className={styles.input}>
<InputWidget type={'TEXT'} icon={mailOpenOutline} placeholder={'Email'} />
</div>
<div className={ styles.info }>
Enter your registered email address to reset password through the OTP
</div>

<div className={styles.actionButtonsHolder}>
<IonButton className={styles.actionButton} onClick={()=>props.changeStep('OTP')} expand='block'> Send OTP </IonButton>
</div>
</section>
</IonContent>
)
}

export default ForgotPasswordPage;

+ 22
- 0
src/pages/forgotPassword/forgotPasswordIndex.tsx 查看文件

@@ -0,0 +1,22 @@
import { IonPage } from '@ionic/react';
import React, { useState } from 'react';

import ForgotPasswordPage from './forgotPassword';
import EnterOTPView from './enterOTP';
import EnterNewPasswordView from './enterNewPassword';


type Props = { };

const ForgotPasswordIndex: React.FC<Props> = () => {
let [forgotPasswordStep, changeStep] = useState('INIT');
return(
<IonPage>
{ forgotPasswordStep === 'INIT' && <ForgotPasswordPage forgotPasswordStep={forgotPasswordStep} changeStep={changeStep}/>}
{ forgotPasswordStep === 'OTP' && <EnterOTPView forgotPasswordStep={forgotPasswordStep} changeStep={changeStep}/>}
{ forgotPasswordStep === 'NEWPASSWORD' && <EnterNewPasswordView />}
</IonPage>
)
}
export default ForgotPasswordIndex;

+ 2
- 58
src/pages/login/Login.module.scss 查看文件

@@ -1,60 +1,3 @@
.upfold {
background-color: var(--charcoal);
height: auto;
transform: translateY(-50vh);
width: 100%;
border-bottom-right-radius: 30px;
border-bottom-left-radius: 30px;
position: relative;
z-index: 1;
box-shadow: 0px 0px 10px 5px var(--black-rock);
animation: riseDown 1s forwards;
display: flex;
align-items: center;
justify-content: center;

.container {
padding: 20px 5%;
text-align: center;
}

h2 {
font-size: 26px;
color: white;
margin: 10px 0;
}

p {
margin: 10px 0;
font-size: 14px;
color: var(--grey-rock);
}

figure {
display: block;
width: 100%;
margin: 10px 0;
animation: fadeIn 1s forwards;
opacity: 0;
transform: translateY(10vh);
}

img {
margin: 0 auto;
width: 50%;
display: block;
}

@keyframes riseDown {
from {
transform: translateY(-50vh);
}
to {
transform: translateY(0vh);
}
}
}

.inputForm { .inputForm {
width: 80%; width: 80%;
margin: 40px auto 0; margin: 40px auto 0;
@@ -106,7 +49,8 @@
font-size: 14px; font-size: 14px;
color: var(--rock); color: var(--rock);


a, span {
a,
span {
color: var(--shamrock); color: var(--shamrock);
text-decoration: none; text-decoration: none;
} }


+ 4
- 3
src/pages/login/Login.tsx 查看文件

@@ -3,6 +3,7 @@ import { personOutline, lockOpenOutline } from 'ionicons/icons';
import React from 'react'; import React from 'react';
import InputWidget from '../../components/input/InputWidget'; import InputWidget from '../../components/input/InputWidget';
import styles from './Login.module.scss'; import styles from './Login.module.scss';
import loginStyles from '../../commonStyles/loginFlow/LoginStyles.module.scss';
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";


type Props = { }; type Props = { };
@@ -11,8 +12,8 @@ const LoginPage: React.FC<Props> = () => {
return ( return (
<IonPage> <IonPage>
<IonContent fullscreen> <IonContent fullscreen>
<section className={styles.upfold}>
<div className={styles.container}>
<section className={loginStyles.upfold}>
<div className={loginStyles.container}>
<figure> <figure>
<img src='assets/images/welcome/upfold.svg' alt='upfold image'/> <img src='assets/images/welcome/upfold.svg' alt='upfold image'/>
</figure> </figure>
@@ -29,7 +30,7 @@ const LoginPage: React.FC<Props> = () => {
<InputWidget type={'PASSWORD'} icon={lockOpenOutline} placeholder={'Password'} /> <InputWidget type={'PASSWORD'} icon={lockOpenOutline} placeholder={'Password'} />
</div> </div>


<div className={ styles.actionLink }> <a> Forgot Password? </a> </div>
<div className={ styles.actionLink }> <Link to='/forgotPassword'> <span> Forgot Password? </span> </Link> </div>


<div className={styles.actionButtonsHolder}> <div className={styles.actionButtonsHolder}>
<IonButton className={styles.actionButton} expand='block'> Login </IonButton> <IonButton className={styles.actionButton} expand='block'> Login </IonButton>


+ 4
- 7
src/pages/signup/AdditionalQuestions.module.scss 查看文件

@@ -88,12 +88,12 @@
transition: box-shadow 0.3s, color 0.3s; transition: box-shadow 0.3s, color 0.3s;


&.optionButton { &.optionButton {
color: white;
color: var(--white);
padding-right: 15px; padding-right: 15px;
} }


&.active { &.active {
color: white;
color: var(--white);
box-shadow: 0px 0px 10px 1px var(--shamrock); box-shadow: 0px 0px 10px 1px var(--shamrock);


ion-icon.checkmark { ion-icon.checkmark {
@@ -125,7 +125,6 @@
} }
} }



ion-range { ion-range {
--bar-height: 5px; --bar-height: 5px;
--bar-background: var(--black-rock); --bar-background: var(--black-rock);
@@ -234,7 +233,6 @@
flex-wrap: wrap; flex-wrap: wrap;
padding: 20px 0; padding: 20px 0;


button { button {
margin: 0 auto; margin: 0 auto;
display: block; display: block;
@@ -280,7 +278,6 @@
} }
} }



.nextButton { .nextButton {
position: fixed; position: fixed;
bottom: 5%; bottom: 5%;
@@ -291,7 +288,7 @@
--border-radius: 30px; --border-radius: 30px;
--box-shadow: none; --box-shadow: none;
--background: var(--shamrock); --background: var(--shamrock);
--color: white;
--color: var(--white);
font-size: 18px; font-size: 18px;
filter: grayscale(0%); filter: grayscale(0%);
transition: filter 0.3s; transition: filter 0.3s;
@@ -356,7 +353,7 @@
list-style: none; list-style: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
color: white;
color: var(--white);
width: 100%; width: 100%;
display: block; display: block;
} }


+ 3
- 2
src/pages/signup/Signup.module.scss 查看文件

@@ -20,7 +20,7 @@


h2 { h2 {
font-size: 26px; font-size: 26px;
color: white;
color: var(--white);
} }


p { p {
@@ -140,7 +140,8 @@
color: var(--rock); color: var(--rock);
margin-top: 30px; margin-top: 30px;


span, a {
span,
a {
color: var(--shamrock); color: var(--shamrock);
text-decoration: none; text-decoration: none;
} }


+ 9
- 3
src/theme/variables.css 查看文件

@@ -1,7 +1,7 @@
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;500;600;700&display=swap');
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;500;600;700&display=swap");


* { * {
font-family: 'Poppins', sans-serif;
font-family: "Poppins", sans-serif;
font-weight: 500; font-weight: 500;
letter-spacing: 0.5px; letter-spacing: 0.5px;
line-height: 1.5; line-height: 1.5;
@@ -17,7 +17,12 @@ ion-button {
text-transform: none; text-transform: none;
} }


h1, h2, h3, h4, h5, h6 {
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 700; font-weight: 700;
} }


@@ -125,4 +130,5 @@ http://ionicframework.com/docs/theming/ */
--ash-dust: #e5e5e5; --ash-dust: #e5e5e5;
--ivory: #f4f4f4; --ivory: #f4f4f4;
--pearl: #f7f7f7; --pearl: #f7f7f7;
--white: #ffffff;
} }

正在加载...
取消
保存