diff --git a/03_source/mobile/src/pages/DemoReactLogin/components/Action.tsx b/03_source/mobile/src/pages/DemoReactLogin/components/Action.tsx
new file mode 100644
index 0000000..2ef32b7
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactLogin/components/Action.tsx
@@ -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 => (
+
+
+
+ {props.message}
+
+ {' '}
+ {props.text} →
+
+
+
+
+);
diff --git a/03_source/mobile/src/pages/DemoReactLogin/components/CustomField.module.scss b/03_source/mobile/src/pages/DemoReactLogin/components/CustomField.module.scss
new file mode 100644
index 0000000..ec37f17
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactLogin/components/CustomField.module.scss
@@ -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;
+}
diff --git a/03_source/mobile/src/pages/DemoReactLogin/components/CustomField.tsx b/03_source/mobile/src/pages/DemoReactLogin/components/CustomField.tsx
new file mode 100644
index 0000000..de5a628
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactLogin/components/CustomField.tsx
@@ -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 (
+
+
+ {field.label}
+ {error && {errorMessage}
}
+
+
+
+ );
+};
+
+export default CustomField;
diff --git a/03_source/mobile/src/pages/DemoReactLogin/components/Wave.tsx b/03_source/mobile/src/pages/DemoReactLogin/components/Wave.tsx
new file mode 100644
index 0000000..6e7c821
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactLogin/components/Wave.tsx
@@ -0,0 +1,13 @@
+export const Wave = (): React.JSX.Element => (
+
+);
diff --git a/03_source/mobile/src/pages/DemoReactLogin/data/fields.tsx b/03_source/mobile/src/pages/DemoReactLogin/data/fields.tsx
new file mode 100644
index 0000000..62f7eac
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactLogin/data/fields.tsx
@@ -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("")
+ }
+ }
+ ];
+}
\ No newline at end of file
diff --git a/03_source/mobile/src/pages/DemoReactLogin/data/utils.tsx b/03_source/mobile/src/pages/DemoReactLogin/data/utils.tsx
new file mode 100644
index 0000000..10a39dd
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactLogin/data/utils.tsx
@@ -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;
+}
\ No newline at end of file
diff --git a/03_source/mobile/src/pages/DemoReactLogin/index.tsx b/03_source/mobile/src/pages/DemoReactLogin/index.tsx
index fbd5d16..3c4c4dc 100644
--- a/03_source/mobile/src/pages/DemoReactLogin/index.tsx
+++ b/03_source/mobile/src/pages/DemoReactLogin/index.tsx
@@ -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 (
-
-
-
-
-
+
+
-
+
+
+
+
+
+
+
+
+
- {/* */}
+ {/*
@@ -33,6 +38,7 @@ function DemoReactLogin() {
Search
+ */}
);
}
diff --git a/03_source/mobile/src/pages/DemoReactLogin/module.d.ts b/03_source/mobile/src/pages/DemoReactLogin/module.d.ts
new file mode 100644
index 0000000..4af7be7
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactLogin/module.d.ts
@@ -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;
+}
diff --git a/03_source/mobile/src/pages/DemoReactLogin/pages/Home.module.scss b/03_source/mobile/src/pages/DemoReactLogin/pages/Home.module.scss
new file mode 100644
index 0000000..e600dab
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactLogin/pages/Home.module.scss
@@ -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;
+}
diff --git a/03_source/mobile/src/pages/DemoReactLogin/pages/Home.tsx b/03_source/mobile/src/pages/DemoReactLogin/pages/Home.tsx
new file mode 100644
index 0000000..e1329e8
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactLogin/pages/Home.tsx
@@ -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 (
+
+
+ {/* */}
+
+ {/* */}
+
+
+
+
+
+
+
+ Join millions of other people discovering their creative side
+
+
+
+
+
+
+
+
+ Get started →
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Home;
diff --git a/03_source/mobile/src/pages/DemoReactLogin/pages/Login.module.scss b/03_source/mobile/src/pages/DemoReactLogin/pages/Login.module.scss
new file mode 100644
index 0000000..344e511
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactLogin/pages/Login.module.scss
@@ -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;
+ }
+}
diff --git a/03_source/mobile/src/pages/DemoReactLogin/pages/Login.tsx b/03_source/mobile/src/pages/DemoReactLogin/pages/Login.tsx
new file mode 100644
index 0000000..bcde778
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactLogin/pages/Login.tsx
@@ -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(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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Log in
+ Welcome back, hope you're doing well
+
+
+
+
+
+ {fields.map((field) => {
+ return ;
+ })}
+
+
+ Login
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Login;
diff --git a/03_source/mobile/src/pages/DemoReactLogin/pages/Signup.module.scss b/03_source/mobile/src/pages/DemoReactLogin/pages/Signup.module.scss
new file mode 100644
index 0000000..0b1f4a7
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactLogin/pages/Signup.module.scss
@@ -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;
+ }
+}
diff --git a/03_source/mobile/src/pages/DemoReactLogin/pages/Signup.tsx b/03_source/mobile/src/pages/DemoReactLogin/pages/Signup.tsx
new file mode 100644
index 0000000..f607d69
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactLogin/pages/Signup.tsx
@@ -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(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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sign up
+ Lets get to know each other
+
+
+
+
+
+ {fields.map((field) => {
+ return ;
+ })}
+
+
+ Create account
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Signup;
diff --git a/03_source/mobile/src/pages/DemoReactLogin/store/AccountStore.tsx b/03_source/mobile/src/pages/DemoReactLogin/store/AccountStore.tsx
new file mode 100644
index 0000000..acfc099
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactLogin/store/AccountStore.tsx
@@ -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) });
+// }
\ No newline at end of file
diff --git a/03_source/mobile/src/pages/DemoReactLogin/store/Selectors.tsx b/03_source/mobile/src/pages/DemoReactLogin/store/Selectors.tsx
new file mode 100644
index 0000000..f6cd2f9
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactLogin/store/Selectors.tsx
@@ -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]);
\ No newline at end of file
diff --git a/03_source/mobile/src/pages/DemoReactLogin/store/index.tsx b/03_source/mobile/src/pages/DemoReactLogin/store/index.tsx
new file mode 100644
index 0000000..d8e5a9b
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactLogin/store/index.tsx
@@ -0,0 +1 @@
+export { default as AccountStore } from "./AccountStore";
\ No newline at end of file
diff --git a/03_source/mobile/src/pages/DemoReactLogin/style.scss b/03_source/mobile/src/pages/DemoReactLogin/style.scss
index 37c1e1a..e69de29 100644
--- a/03_source/mobile/src/pages/DemoReactLogin/style.scss
+++ b/03_source/mobile/src/pages/DemoReactLogin/style.scss
@@ -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;
-}
diff --git a/03_source/mobile/src/pages/DemoReactLogin/theme/variables.scss b/03_source/mobile/src/pages/DemoReactLogin/theme/variables.scss
new file mode 100644
index 0000000..e69de29