update demo-react-login,

This commit is contained in:
louiscklaw
2025-06-08 19:07:38 +08:00
parent 2862cddb6b
commit 0d844eed3f
19 changed files with 643 additions and 115 deletions

View File

@@ -0,0 +1,21 @@
import { IonCol, IonRouterLink, IonRow } from '@ionic/react';
interface ActionProps {
message: string;
text: string;
link: string;
}
export const Action = (props: ActionProps): React.JSX.Element => (
<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>
);

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,38 @@
import { IonInput, IonLabel } from '@ionic/react';
import styles from './CustomField.module.scss';
interface FieldType {
id: string;
label: string;
input: {
props: any;
state: any;
};
}
interface ErrorType {
id: string;
message: string;
}
interface CustomFieldProps {
field: FieldType;
errors?: ErrorType[];
}
const CustomField = ({ field, errors }: CustomFieldProps): 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 default CustomField;

View File

@@ -0,0 +1,13 @@
export const Wave = (): React.JSX.Element => (
<svg
style={{ marginBottom: '-0.5rem' }}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1440 320"
>
<path
fill="#7a506f"
fill-opacity="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,82 @@
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,46 @@
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

@@ -1,28 +1,33 @@
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
import { IonRouterOutlet, IonTabs } from '@ionic/react';
import { cloudOutline, searchOutline } from 'ionicons/icons';
import { Route, Redirect } from 'react-router';
import Tab1 from './AppPages/Tab1';
import Tab2 from './AppPages/Tab2';
import './theme/variables.scss';
import './style.scss';
import Home from './pages/Home';
import Login from './pages/Login';
import Signup from './pages/Signup';
function DemoReactLogin() {
return (
<IonTabs>
<IonRouterOutlet>
<Route exact path="/demo-react-login/tab1">
<Tab1 />
</Route>
<Route exact path="/demo-react-login/tab2">
<Tab2 />
<Route exact path="/demo-react-login/home">
<Home />
</Route>
<Redirect exact path="/demo-react-login" to="/demo-react-login/tab1" />
<Route exact path="/demo-react-login/signup">
<Signup />
</Route>
<Route exact path="/demo-react-login/login">
<Login />
</Route>
<Redirect exact path="/demo-react-login" to="/demo-react-login/home" />
</IonRouterOutlet>
{/* */}
{/*
<IonTabBar slot="bottom">
<IonTabButton tab="tab1" href="/demo-react-login/tab1">
<IonIcon icon={cloudOutline} />
@@ -33,6 +38,7 @@ function DemoReactLogin() {
<IonLabel>Search</IonLabel>
</IonTabButton>
</IonTabBar>
*/}
</IonTabs>
);
}

View File

@@ -0,0 +1,9 @@
declare module '*.module.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.module.scss' {
const classes: { readonly [key: string]: string };
export default classes;
}

View File

@@ -0,0 +1,38 @@
.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;
}

View File

@@ -0,0 +1,59 @@
import {
IonButton,
IonCardTitle,
IonCol,
IonContent,
IonFooter,
IonGrid,
IonHeader,
IonImg,
IonPage,
IonRouterLink,
IonRow,
IonToolbar,
} from '@ionic/react';
import { Action } from '../components/Action';
import styles from './Home.module.scss';
const Home = (): React.JSX.Element => {
return (
<IonPage className={styles.homePage}>
<IonHeader>
{/* <IonToolbar className="ion-no-margin ion-no-padding"> */}
<IonImg src="/assets/DemoReactLogin/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 Home;

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,112 @@
import {
IonBackButton,
IonButton,
IonButtons,
IonCardTitle,
IonCol,
IonContent,
IonFooter,
IonGrid,
IonHeader,
IonIcon,
IonPage,
IonRouterLink,
IonRow,
IonToolbar,
} from '@ionic/react';
import styles from './Login.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 { validateForm } from '../data/utils';
import { useParams } from 'react-router';
interface FieldType {
id: string;
label: string;
input: {
props: any;
state: any;
};
}
interface ErrorType {
id: string;
message: string;
}
const Login = (): React.JSX.Element => {
const params = useParams();
const fields: FieldType[] = useLoginFields();
const [errors, setErrors] = useState<ErrorType[] | false>(false);
const login = (): void => {
const errors = validateForm(fields);
setErrors(errors);
if (!errors.length) {
// Submit your form here
}
};
useEffect(() => {
return () => {
fields.forEach((field) => field.input.state.reset(''));
setErrors(false);
};
}, [params]);
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">
{fields.map((field) => {
return <CustomField field={field} errors={errors} />;
})}
<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="/signup" />
<Wave />
</IonGrid>
</IonFooter>
</IonPage>
);
};
export default Login;

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;
}
}

View File

@@ -0,0 +1,111 @@
import {
IonBackButton,
IonButton,
IonButtons,
IonCardTitle,
IonCol,
IonContent,
IonFooter,
IonGrid,
IonHeader,
IonIcon,
IonPage,
IonRouterLink,
IonRow,
IonToolbar,
} from '@ionic/react';
import styles from './Signup.module.scss';
import { arrowBack, shapesOutline } from 'ionicons/icons';
import CustomField from '../components/CustomField';
import { useSignupFields } from '../data/fields';
import { Action } from '../components/Action';
import { Wave } from '../components/Wave';
import { useEffect, useState } from 'react';
import { validateForm } from '../data/utils';
import { useParams } from 'react-router';
interface FieldType {
id: string;
label: string;
input: {
props: any;
state: any;
};
}
interface ErrorType {
id: string;
message: string;
}
const Signup = (): React.JSX.Element => {
const params = useParams();
const fields: FieldType[] = useSignupFields();
const [errors, setErrors] = useState<ErrorType[] | false>(false);
const createAccount = (): void => {
const errors = validateForm(fields);
setErrors(errors);
if (!errors.length) {
// Submit your form here
}
};
useEffect(() => {
return () => {
fields.forEach((field) => field.input.state.reset(''));
setErrors(false);
};
}, [params]);
return (
<IonPage className={styles.signupPage}>
<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) => {
return <CustomField field={field} errors={errors} />;
})}
<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="/login" />
<Wave />
</IonGrid>
</IonFooter>
</IonPage>
);
};
export default Signup;

View File

@@ -0,0 +1,19 @@
import { Store } from "pullstate";
const AccountStore = new Store({
logged_in: false,
coffee_ids: []
});
export default AccountStore;
// export const addToCart = (coffeeID) => {
// CartStore.update(s => { s.coffee_ids = [ ...s.coffee_ids, `${ parseInt(coffeeID) }` ]; });
// }
// export const removeFromCart = coffeeIndex => {
// CartStore.update(s => { s.coffee_ids.splice(coffeeIndex, 1) });
// }

View File

@@ -0,0 +1,13 @@
import { createSelector } from 'reselect';
const getState = state => state;
// General getters
// export const getCoffees = createSelector(getState, state => state.coffees);
// export const getOffers = createSelector(getState, state => state.offers);
// export const getCoffeeSizes = createSelector(getState, state => state.sizes);
// export const getCartCoffees = createSelector(getState, state => state.coffee_ids);
// export const getFavouriteCoffees = createSelector(getState, state => state.coffee_ids);
// // More specific getters
// export const getCoffee = id => createSelector(getState, state => state.coffees.filter(c => parseInt(c.id) === parseInt(id))[0]);

View File

@@ -0,0 +1 @@
export { default as AccountStore } from "./AccountStore";

View File

@@ -1,103 +0,0 @@
#about-page {
ion-toolbar {
position: absolute;
top: 0;
left: 0;
right: 0;
--background: transparent;
--color: white;
}
ion-toolbar ion-back-button,
ion-toolbar ion-button,
ion-toolbar ion-menu-button {
--color: white;
}
.about-header {
position: relative;
width: 100%;
height: 30%;
}
.about-header .about-image {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
opacity: 0;
transition: opacity 500ms ease-in-out;
}
.about-header .madison {
background-image: url('/assets/WeatherDemo/img/about/madison.jpg');
}
.about-header .austin {
background-image: url('/assets/WeatherDemo/img/about/austin.jpg');
}
.about-header .chicago {
background-image: url('/assets/WeatherDemo/img/about/chicago.jpg');
}
.about-header .seattle {
background-image: url('/assets/WeatherDemo/img/about/seattle.jpg');
}
.about-info {
position: relative;
margin-top: -10px;
border-radius: 10px;
background: var(--ion-background-color, #fff);
z-index: 2; // display rounded border above header image
}
.about-info h3 {
margin-top: 0;
}
.about-info ion-list {
padding-top: 0;
}
.about-info p {
line-height: 130%;
color: var(--ion-color-dark);
}
.about-info ion-icon {
margin-inline-end: 32px;
}
/*
* iOS Only
*/
.ios .about-info {
--ion-padding: 19px;
}
.ios .about-info h3 {
font-weight: 700;
}
}
#date-input-popover {
--offset-y: -var(--ion-safe-area-bottom);
--max-width: 90%;
--width: 336px;
}