add authentication routes, components, and pages including AuthHome, Login, and SignUp, and implement form fields utility functions
```
This commit is contained in:
louiscklaw
2025-05-12 19:22:00 +08:00
parent ee0aa0353b
commit 3f10a0728c
14 changed files with 543 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

View File

@@ -21,6 +21,10 @@ import ConnectivesWordPage from './pages/Favorite/ConnectivesWordPage';
import FavVocabularyPage from './pages/Favorite/Vocabulary';
import FavoriteVocabularyPage from './pages/Favorite/WordPage';
import ConnectivesPage from './pages/Lesson/ConnectivesPage';
import AuthHome from './pages/auth/Home';
import { AuthLogin } from './pages/auth/Login';
import { AuthSignUp } from './pages/auth/SignUp';
import Lesson from './pages/Lesson/index';
// NOTES: old version using json file
@@ -156,6 +160,18 @@ function RouteConfig() {
<ConnectivesPage />
</Route>
<Route exact path={`/auth/Home`}>
<AuthHome />
</Route>
<Route exact path={`/auth/login`}>
<AuthLogin />
</Route>
<Route exact path={`/auth/signup`}>
<AuthSignUp />
</Route>
{/* TODO: remove below */}
<Route exact path="/tab1">
<Tab1 />

View File

@@ -0,0 +1,21 @@
import { IonCol, IonRouterLink, IonRow } from '@ionic/react';
function Action(props: { message: string; text: string; link: string }): React.JSX.Element {
return (
<>
<IonRow className="ion-text-center ion-justify-content-center">
<IonCol size="12">
<p>
{props.message}
<IonRouterLink className="custom-link" routerLink={props.link}>
{' '}
{props.text} &rarr;
</IonRouterLink>
</p>
</IonCol>
</IonRow>
</>
);
}
export { Action };

View File

@@ -0,0 +1,40 @@
import { IonInput, IonLabel } from '@ionic/react';
import styles from './style.module.scss';
function CustomField({
field,
errors,
}: {
field: {
id: string;
label: string;
required: boolean;
input: {
props: { type: string; placeholder: string };
state: {
value: string;
reset: (newValue: any) => void;
onIonChange: (e: any) => Promise<void>;
onKeyUp: (e: any) => Promise<void>;
};
};
};
errors: any;
}): React.JSX.Element {
const error = errors && errors.filter((e) => e.id === field.id)[0];
const errorMessage = error && errors.filter((e) => e.id === field.id)[0].message;
return (
<>
<div className={styles.field}>
<IonLabel className={styles.fieldLabel}>
{field.label}
{error && <p className="animate__animated animate__bounceIn">{errorMessage}</p>}
</IonLabel>
<IonInput className={styles.customInput} {...field.input.props} {...field.input.state} />
</div>
</>
);
}
export { CustomField };

View File

@@ -0,0 +1,29 @@
.field:not(:last-child) {
margin-bottom: 1rem !important;
}
.field {
ion-label {
padding-left: 0.2rem;
padding-right: 0.5rem;
color: #d3a6c7;
display: flex;
justify-content: space-between;
align-content: center;
align-items: center;
p {
color: rgb(236, 149, 35);
}
}
}
.customInput {
// --background: #834e76;
--padding-bottom: 1rem;
--padding-top: 1rem;
--padding-start: 1rem;
--padding-end: 1rem;
border-radius: 10px;
margin-top: 0.25rem;
transition: all 0.2s linear;
}

View File

@@ -0,0 +1,9 @@
export const Wave = () => (
<svg style={{ marginBottom: '-0.5rem' }} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320">
<path
fill="#7a506f"
fillOpacity="1"
d="M0,288L40,277.3C80,267,160,245,240,224C320,203,400,181,480,176C560,171,640,181,720,181.3C800,181,880,171,960,144C1040,117,1120,75,1200,58.7C1280,43,1360,53,1400,58.7L1440,64L1440,320L1400,320C1360,320,1280,320,1200,320C1120,320,1040,320,960,320C880,320,800,320,720,320C640,320,560,320,480,320C400,320,320,320,240,320C160,320,80,320,40,320L0,320Z"
></path>
</svg>
);

View File

@@ -0,0 +1,71 @@
import { useFormInput } from './utils';
export const useSignupFields = () => {
return [
{
id: 'name',
label: 'Name',
required: true,
input: {
props: {
type: 'text',
placeholder: 'Joe Bloggs',
},
state: useFormInput(''),
},
},
{
id: 'email',
label: 'Email',
required: true,
input: {
props: {
type: 'email',
placeholder: 'joe@bloggs.com',
},
state: useFormInput(''),
},
},
{
id: 'password',
label: 'Password',
required: true,
input: {
props: {
type: 'password',
placeholder: '*********',
},
state: useFormInput(''),
},
},
];
};
export const useLoginFields = () => {
return [
{
id: 'email',
label: 'Email',
required: true,
input: {
props: {
type: 'email',
placeholder: 'joe@bloggs.com',
},
state: useFormInput(''),
},
},
{
id: 'password',
label: 'Password',
required: true,
input: {
props: {
type: 'password',
placeholder: '*******',
},
state: useFormInput(''),
},
},
];
};

View File

@@ -0,0 +1,38 @@
import { useState } from 'react';
export const useFormInput = (initialValue = '') => {
const [value, setValue] = useState(initialValue);
const handleChange = async (e) => {
const tempValue = await e.currentTarget.value;
setValue(tempValue);
};
return {
value,
reset: (newValue) => setValue(newValue),
onIonChange: handleChange,
onKeyUp: handleChange,
};
};
export const validateForm = (fields) => {
let errors = [];
fields.forEach((field) => {
if (field.required) {
const fieldValue = field.input.state.value;
if (fieldValue === '') {
const error = {
id: field.id,
message: `Please check your ${field.id}`,
};
errors.push(error);
}
}
});
return errors;
};

View File

@@ -0,0 +1,56 @@
import {
IonButton,
IonCardTitle,
IonCol,
IonContent,
IonFooter,
IonGrid,
IonHeader,
IonImg,
IonPage,
IonRouterLink,
IonRow,
IonToolbar,
} from '@ionic/react';
// import { Action } from '../components/Action';
import styles from './style.module.scss';
import { Action } from '../../../components/Action';
const AuthHome = () => {
return (
<IonPage className={'styles.homePage'}>
<IonHeader>
{/* <IonToolbar className="ion-no-margin ion-no-padding"> */}
<IonImg src="/assets/login2.jpeg" />
{/* </IonToolbar> */}
</IonHeader>
<IonContent fullscreen>
<div className={styles.getStarted}>
<IonGrid>
<IonRow className={`ion-text-center ion-justify-content-center ${styles.heading}`}>
<IonCol size="11" className={styles.headingText}>
<IonCardTitle>Join millions of other people discovering their creative side</IonCardTitle>
</IonCol>
</IonRow>
<IonRow className={`ion-text-center ion-justify-content-center`}>
<IonRouterLink routerLink="/signup" className="custom-link">
<IonCol size="11">
<IonButton className={`${styles.getStartedButton} custom-button`}>Get started &rarr;</IonButton>
</IonCol>
</IonRouterLink>
</IonRow>
</IonGrid>
</div>
</IonContent>
<IonFooter>
<IonGrid>
<Action message="Already got an account?" text="Login" link="/login" />
</IonGrid>
</IonFooter>
</IonPage>
);
};
export default AuthHome;

View File

@@ -0,0 +1,41 @@
.homePage {
ion-header {
ion-img {
border-bottom: 3px solid rgb(236, 149, 35);
}
}
ion-footer {
// background-color: #7c3b6a;
color: white;
}
}
.getStarted {
height: 100%;
background-color: #ffffff;
background-image: radial-gradient(#b8b8b8 1px, transparent 1px), radial-gradient(#b8b8b8 1px, #ffffff 1px);
background-size: 40px 40px;
background-position:
0 0,
20px 20px;
ion-card-title {
color: black !important;
letter-spacing: -0.08rem;
font-weight: 900 !important;
}
}
.heading {
margin-top: 7rem;
}
.getStartedButton {
font-size: 1.2rem;
margin-top: 1rem;
}
.helloworld {
color: gold;
}

View File

@@ -0,0 +1,93 @@
import {
IonBackButton,
IonButton,
IonButtons,
IonCardTitle,
IonCol,
IonContent,
IonFooter,
IonGrid,
IonHeader,
IonIcon,
IonImg,
IonInput,
IonInputPasswordToggle,
IonPage,
IonRouterLink,
IonRow,
IonText,
IonToolbar,
} from '@ionic/react';
import styles from './style.module.scss';
import { arrowBack, shapesOutline } from 'ionicons/icons';
import { CustomField } from '../../../components/CustomField';
import { useLoginFields } from '../../../data/fields';
import { Action } from '../../../components/Action';
import { Wave } from '../../../components/Wave';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router';
function AuthLogin(): React.JSX.Element {
const params = useParams();
const [errors, setErrors] = useState(false);
const login = () => {};
return (
<IonPage className={styles.loginPage}>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton icon={arrowBack} text="" className="custom-back" />
</IonButtons>
<IonButtons slot="end">
<IonButton className="custom-button">
<IonIcon icon={shapesOutline} />
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
{/* */}
<IonContent fullscreen>
<IonGrid className="ion-padding">
<IonRow>
<IonCol size="12" className={styles.headingText}>
<IonCardTitle>Log in</IonCardTitle>
<h5>Welcome back, hope you're doing well</h5>
</IonCol>
</IonRow>
<IonRow className="ion-margin-top ion-padding-top">
<IonCol size="12">
<IonInput labelPlacement="floating" value="hi@ionic.io">
<div slot="label">
Email <IonText color="danger">(Required)</IonText>
</div>
</IonInput>
<IonInput type="password" label="Password" value="NeverGonnaGiveYouUp">
<IonInputPasswordToggle slot="end"></IonInputPasswordToggle>
</IonInput>
<IonButton className="custom-button" expand="block" onClick={login}>
Login
</IonButton>
</IonCol>
</IonRow>
</IonGrid>
</IonContent>
{/* */}
<IonFooter>
<IonGrid className="ion-no-margin ion-no-padding">
<Action message="Don't have an account?" text="Sign up" link="/auth/signup" />
<Wave />
</IonGrid>
</IonFooter>
</IonPage>
);
}
export { AuthLogin };

View File

@@ -0,0 +1,17 @@
.loginPage {
ion-toolbar {
--border-style: none;
--border-color: transparent;
--padding-top: 1rem;
--padding-bottom: 1rem;
--padding-start: 1rem;
--padding-end: 1rem;
}
}
.headingText {
h5 {
margin-top: 0.2rem;
// color: #d3a6c7;
}
}

View File

@@ -0,0 +1,95 @@
import {
IonBackButton,
IonButton,
IonButtons,
IonCardTitle,
IonCol,
IonContent,
IonFooter,
IonGrid,
IonHeader,
IonIcon,
IonImg,
IonInput,
IonInputPasswordToggle,
IonPage,
IonRouterLink,
IonRow,
IonText,
IonToolbar,
} from '@ionic/react';
import styles from './style.module.scss';
import { arrowBack, shapesOutline } from 'ionicons/icons';
import { CustomField } from '../../../components/CustomField';
import { useLoginFields, useSignupFields } from '../../../data/fields';
import { Action } from '../../../components/Action';
import { Wave } from '../../../components/Wave';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router';
function AuthSignUp(): React.JSX.Element {
const params = useParams();
const fields = useSignupFields();
const [errors, setErrors] = useState(false);
const login = () => {};
function createAccount() {}
return (
<IonPage className={styles.loginPage}>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton icon={arrowBack} text="" className="custom-back" />
</IonButtons>
<IonButtons slot="end">
<IonButton className="custom-button">
<IonIcon icon={shapesOutline} />
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
{/* */}
<IonContent fullscreen>
<IonGrid className="ion-padding">
<IonRow>
<IonCol size="12" className={styles.headingText}>
<IonCardTitle>Sign up</IonCardTitle>
<h5>Lets get to know each other</h5>
</IonCol>
</IonRow>
<IonRow className="ion-margin-top ion-padding-top">
<IonCol size="12">
{fields.map((field, i) => {
return (
<div key={i}>
<CustomField field={field} errors={errors} />
</div>
);
})}
<IonButton className="custom-button" expand="block" onClick={createAccount}>
Create account
</IonButton>
</IonCol>
</IonRow>
</IonGrid>
</IonContent>
{/* */}
<IonFooter>
<IonGrid className="ion-no-margin ion-no-padding">
<Action message="Already got an account?" text="Login" link="/auth/login" />
<Wave />
</IonGrid>
</IonFooter>
</IonPage>
);
}
export { AuthSignUp };

View File

@@ -0,0 +1,17 @@
.signupPage {
ion-toolbar {
--border-style: none;
--border-color: transparent;
--padding-top: 1rem;
--padding-bottom: 1rem;
--padding-start: 1rem;
--padding-end: 1rem;
}
}
.headingText {
h5 {
margin-top: 0.2rem;
// color: #d3a6c7;
}
}