diff --git a/03_source/mobile/src/pages/DemoReactShopUi/components/AddToCartButton.jsx b/03_source/mobile/src/pages/DemoReactShopUi/components/AddToCartButton.jsx
new file mode 100644
index 0000000..33810a5
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactShopUi/components/AddToCartButton.jsx
@@ -0,0 +1,58 @@
+import { CreateAnimation, IonButton, IonIcon } from "@ionic/react";
+import { cartOutline } from "ionicons/icons";
+import { useRef, useState } from "react";
+import { addToCart } from "../store/CartStore";
+
+export const AddToCartButton = ({product}) => {
+
+ const animationRef = useRef();
+ const [hidden, setHidden] = useState(true);
+
+ const floatStyle = {
+
+ display: hidden ? "none" : "",
+ position: "absolute"
+ };
+
+ const floatGrowAnimation = {
+
+ property: "transform",
+ fromValue: "translateY(0) scale(1)",
+ toValue: "translateY(-55px) scale(1.5)"
+ };
+
+ const colorAnimation = {
+
+ property: "color",
+ fromValue: "green",
+ toValue: "green"
+ };
+
+ const mainAnimation = {
+
+ duration: 1500,
+ iterations: "1",
+ fromTo: [ floatGrowAnimation, colorAnimation ],
+ easing: "cubic-bezier(0.25, 0.7, 0.25, 0.7)"
+ };
+
+ const handleAddToCart = async product => {
+
+ setHidden(false);
+ await animationRef.current.animation.play();
+ setHidden(true);
+ addToCart(product);
+ }
+
+ return (
+
+ handleAddToCart(product)}>
+
+ Add to Cart
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/03_source/mobile/src/pages/DemoReactShopUi/components/Breadcrumbs.jsx b/03_source/mobile/src/pages/DemoReactShopUi/components/Breadcrumbs.jsx
new file mode 100644
index 0000000..a5b3263
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactShopUi/components/Breadcrumbs.jsx
@@ -0,0 +1,23 @@
+import { IonBreadcrumb, IonBreadcrumbs, IonIcon } from "@ionic/react";
+import { fastFoodOutline } from "ionicons/icons";
+import { useState } from "react";
+
+export const Breadcrumbs = () => {
+
+ const [maxItems, setMaxItems] = useState(2);
+
+ const handleClick = () => {
+
+ setMaxItems(undefined);
+ }
+
+ return (
+
+
+ Page 1
+ Page 2
+ Page 3
+ Page 4
+
+ );
+}
\ No newline at end of file
diff --git a/03_source/mobile/src/pages/DemoReactShopUi/components/CartModal.jsx b/03_source/mobile/src/pages/DemoReactShopUi/components/CartModal.jsx
new file mode 100644
index 0000000..ce7c030
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactShopUi/components/CartModal.jsx
@@ -0,0 +1,82 @@
+import { useStoreState } from "pullstate";
+import { useEffect, useState } from "react";
+import { CartStore } from "../store";
+import { addToCart } from "../store/CartStore";
+import { getCart } from "../store/Selectors";
+
+const { IonPage, IonHeader, IonToolbar, IonTitle, IonButtons, IonIcon, IonContent, IonGrid, IonRow, IonItem, IonLabel, IonText, IonThumbnail, IonFooter, IonCol, IonButton, IonItemSliding, IonItemOptions, IonItemOption } = require("@ionic/react");
+const { close } = require("ionicons/icons");
+
+export const CartModal = props => {
+
+ const cart = useStoreState(CartStore, getCart);
+ const [totalPrice, setTotalPrice] = useState(0);
+
+ useEffect(() => {
+
+ let total = 0;
+ cart.forEach(item => total += parseInt(item.price.replace("£", "")));
+ setTotalPrice(total);
+ }, [cart]);
+
+ return (
+
+
+
+ Cart
+
+
+
+
+
+
+
+
+
+
+
+ {cart.length} products in your cart
+
+ Review products and checkout
+
+
+
+
+
+
+ {cart.map((item, index) => (
+
+
+
+
+
+
+ {item.title}
+ {item.price}
+
+
+
+
+ addToCart(item)}>
+ Remove
+
+
+
+ ))}
+
+
+
+
+
+ Total
+
+
+
+ £{totalPrice.toFixed(2)}
+
+
+ Checkout →
+
+
+ );
+}
\ No newline at end of file
diff --git a/03_source/mobile/src/pages/DemoReactShopUi/components/FilterModal.jsx b/03_source/mobile/src/pages/DemoReactShopUi/components/FilterModal.jsx
new file mode 100644
index 0000000..067aac7
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactShopUi/components/FilterModal.jsx
@@ -0,0 +1,36 @@
+import { IonButton, IonCol, IonContent, IonGrid, IonHeader, IonRow, IonTitle, IonToolbar } from "@ionic/react";
+
+export const FilterModal = ({productsRef, filterCriteria, setFilterCriteria, dismiss, filters}) => {
+
+ const filterProducts = async filter => {
+
+ await productsRef.current.classList.add("animate__fadeOutLeft");
+
+ setTimeout(() => {
+ productsRef.current.classList.remove("animate__fadeOutLeft");
+ productsRef.current.classList.add("animate__fadeInRight");
+ setFilterCriteria(filter);
+ }, 500);
+ dismiss();
+ }
+
+ return (
+
+
+
+
+ Filter
+
+
+
+
+ {filters.map(f => (
+
+ filterProducts(f)}>{f}
+
+ ))}
+
+
+
+ );
+ }
\ No newline at end of file
diff --git a/03_source/mobile/src/pages/DemoReactShopUi/components/ProductModal.css b/03_source/mobile/src/pages/DemoReactShopUi/components/ProductModal.css
new file mode 100644
index 0000000..8884088
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactShopUi/components/ProductModal.css
@@ -0,0 +1,88 @@
+ion-card {
+ margin: 0;
+ /* margin-top: var(--ion-safe-area-top); */
+ z-index: -1;
+
+ border-radius: 0px;
+
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ box-shadow: none;
+ aspect-ratio: 1 / 1;
+}
+
+@supports not (aspect-ratio: 1 / 1) {
+ ion-card::before {
+ float: left;
+ padding-top: 100%;
+ content: '';
+ }
+
+ ion-card::after {
+ display: block;
+ content: '';
+ clear: both;
+ }
+}
+
+ion-card-header {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+
+ /* background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.7) 100%); */
+ background: rgba(0, 0, 0, 0.5)
+}
+
+ion-card-title,
+ion-card-subtitle {
+ color: white;
+}
+
+ion-card-header ion-card-title {
+
+ margin: 0 0 6px 0;
+ font-size: 22px;
+}
+
+ion-card-header ion-card-subtitle {
+
+ text-transform: none;
+ font-weight: 500;
+ font-size: 16px;
+}
+
+ion-card-content {
+ height: calc(60px + var(--ion-safe-area-top));
+ background: linear-gradient(0deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.5) 100%);
+}
+
+#close-button {
+ position: fixed;
+
+ top: max(var(--ion-safe-area-top), 16px);
+ right: 8px;
+}
+
+#fave-button {
+ position: fixed;
+
+ top: max(var(--ion-safe-area-top), 16px);
+ left: 8px;
+}
+
+#product-view-buttons {
+
+ z-index: 10;
+ background: linear-gradient(360deg, rgba(0, 0, 0, 0) 0%, rgba(82, 82, 82, 0.9) 100%) !important;
+ position: absolute;
+ width: 100%;
+ height: 4rem;
+}
+
+.sticky-bottom {
+
+ position: fixed;
+ bottom: 0;
+}
\ No newline at end of file
diff --git a/03_source/mobile/src/pages/DemoReactShopUi/components/ProductModal.jsx b/03_source/mobile/src/pages/DemoReactShopUi/components/ProductModal.jsx
new file mode 100644
index 0000000..a519b87
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactShopUi/components/ProductModal.jsx
@@ -0,0 +1,76 @@
+import { IonButton, IonButtons, IonCard, IonCardHeader, IonCardSubtitle, IonCardTitle, IonCol, IonContent, IonFooter, IonIcon, IonLabel, IonNote, IonRow, IonText, IonToolbar } from "@ionic/react";
+import { closeCircle, heart, heartOutline } from "ionicons/icons";
+import { useStoreState } from "pullstate";
+import { useRef } from "react";
+
+import { checkFavourites } from "../store/Selectors";
+import { addToFavourites } from "../store/FavouritesStore";
+import { FavouritesStore } from "../store";
+
+import "./ProductModal.css";
+import { ProductReviews } from "./ProductReviews";
+import { ProductSpecificationsAccordion } from "./ProductSpecificationsAccordion";
+import { AddToCartButton } from "./AddToCartButton";
+
+export const ProductModal = props => {
+
+ const { dismiss, category = false, product } = props;
+ const isFavourite = useStoreState(FavouritesStore, checkFavourites(product));
+ const contentRef = useRef(null);
+
+ return (
+ <>
+
+
+
+
+
+
+ addToFavourites(product, category)} id="fave-button">
+
+
+
+
+
+
+ {product.title}
+ {product.price}
+
+
+
+
+
+
+
+
+ shop
+ {category ? category : "Favourite"}
+
+
+
+
+
+
Product Description
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam elit felis, molestie id venenatis at, commodo ac tortor. Pellentesque tempus aliquet purus, sed vulputate elit tempus ut.
+
+
Product Specifications
+
+
+
+
+
+
+
+
+ {product.price}
+
+
+
+
+
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/03_source/mobile/src/pages/DemoReactShopUi/components/ProductReviews.jsx b/03_source/mobile/src/pages/DemoReactShopUi/components/ProductReviews.jsx
new file mode 100644
index 0000000..d564773
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactShopUi/components/ProductReviews.jsx
@@ -0,0 +1,23 @@
+import { IonCol, IonIcon, IonNote } from "@ionic/react";
+import { star } from "ionicons/icons";
+import { useEffect, useState } from "react";
+import { randomCount } from "../utils";
+
+export const ProductReviews = () => {
+
+ // This count could come from the product (if real data was fed)
+ const [reviewCount, setReviewCount] = useState(0);
+
+ useEffect(() => {
+
+ setReviewCount(randomCount());
+ }, []);
+
+ return (
+
+
+
+ {reviewCount} review{reviewCount > 1 && "s"}
+
+ );
+}
\ No newline at end of file
diff --git a/03_source/mobile/src/pages/DemoReactShopUi/components/ProductSpecificationsAccordion.jsx b/03_source/mobile/src/pages/DemoReactShopUi/components/ProductSpecificationsAccordion.jsx
new file mode 100644
index 0000000..77ede79
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactShopUi/components/ProductSpecificationsAccordion.jsx
@@ -0,0 +1,58 @@
+import { IonAccordion, IonAccordionGroup, IonItem, IonLabel, IonList, IonNote } from "@ionic/react";
+import { useRef } from "react";
+import { productSpecs } from "../utils";
+
+export const ProductSpecificationsAccordion = ({type, contentRef}) => {
+
+ const accordionGroupRef = useRef(null);
+
+ const log = () => {
+
+ const selectedAccordion = accordionGroupRef.current.value;
+
+ if (selectedAccordion) {
+
+ setTimeout(() => contentRef.current.scrollToBottom(400), 200);
+ }
+ }
+
+ return (
+
+ {Object.keys(productSpecs).map((spec, index) => {
+
+ const {header, options, wrapText = false, noteColor = false} = productSpecs[spec];
+
+ return (
+
+
+
+ {header}
+
+
+
+
+ {options.map((option, index2) => {
+
+ const {label, value} = option;
+
+ return (
+
+
+
+ {label}
+
+
+
+ {noteColor ? (value ? "In stock" : "Out of stock") : value}
+
+
+
+ );
+ })}
+
+
+ );
+ })}
+
+ );
+}
\ No newline at end of file
diff --git a/03_source/mobile/src/pages/DemoReactShopUi/pages/Categories.jsx b/03_source/mobile/src/pages/DemoReactShopUi/pages/Categories.jsx
new file mode 100644
index 0000000..fe5fa00
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactShopUi/pages/Categories.jsx
@@ -0,0 +1,45 @@
+import { IonButton, IonContent, IonHeader, IonLabel, IonNote, IonPage, IonRouterLink, IonRow, IonTitle, IonToolbar } from '@ionic/react';
+import { capitalize, productInfo } from '../utils';
+
+import styles from "./Categories.module.scss";
+
+const Categories = () => {
+
+ const categories = Object.keys(productInfo);
+
+ return (
+
+
+
+ Ionic Shop
+
+
+
+
+
+
+ ionic
+ shop
+
+
+
+
+
+ {categories.map(category => (
+
+
+
+

+
{capitalize(category)}
+
+
+
+ // {capitalize(c)}
+ ))}
+
+
+
+ );
+};
+
+export default Categories;
diff --git a/03_source/mobile/src/pages/DemoReactShopUi/pages/Categories.module.scss b/03_source/mobile/src/pages/DemoReactShopUi/pages/Categories.module.scss
new file mode 100644
index 0000000..e5964ba
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactShopUi/pages/Categories.module.scss
@@ -0,0 +1,20 @@
+.categoryContainer {
+
+ display: flex;
+ color: white;
+}
+
+.categoryContainer p {
+
+ display: flex;
+ justify-items: center;
+ justify-content: center;
+ position: absolute;
+
+ background-color: rgba(0, 0, 0, 0.4);
+ width: 50%;
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ margin: 0 auto;
+ font-size: 2rem;
+}
\ No newline at end of file
diff --git a/03_source/mobile/src/pages/DemoReactShopUi/pages/Category.jsx b/03_source/mobile/src/pages/DemoReactShopUi/pages/Category.jsx
new file mode 100644
index 0000000..4915602
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactShopUi/pages/Category.jsx
@@ -0,0 +1,54 @@
+import { IonButton, IonButtons, IonContent, IonHeader, IonIcon, IonLabel, IonNote, IonPage, IonRouterLink, IonRow, IonTitle, IonToolbar, useIonRouter } from '@ionic/react';
+import { chevronBack } from 'ionicons/icons';
+import { useParams } from 'react-router';
+import { capitalize, productInfo } from '../utils';
+
+import styles from "./Categories.module.scss";
+
+const Category = () => {
+
+ const router = useIonRouter();
+ const { category } = useParams();
+ const productTypes = Object.keys(productInfo[category].productTypes);
+
+ return (
+
+
+
+
+
+ router.goBack()}>
+
+ Back
+
+
+ {category}
+
+
+
+
+
+
+ shop
+ {category}
+
+
+
+
+
+ {productTypes.map(product => (
+
+
+
+

+
{capitalize(product)}
+
+
+ ))}
+
+
+
+ );
+};
+
+export default Category;
diff --git a/03_source/mobile/src/pages/DemoReactShopUi/pages/Favourites.jsx b/03_source/mobile/src/pages/DemoReactShopUi/pages/Favourites.jsx
new file mode 100644
index 0000000..39b13ff
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactShopUi/pages/Favourites.jsx
@@ -0,0 +1,77 @@
+import { IonCol, IonContent, IonGrid, IonHeader, IonIcon, IonImg, IonLabel, IonPage, IonRow, IonText, IonTitle, IonToolbar, useIonModal } from '@ionic/react';
+import { heartOutline } from 'ionicons/icons';
+import { useStoreState } from 'pullstate';
+import { useState } from 'react';
+import { ProductModal } from '../components/ProductModal';
+import { FavouritesStore } from '../store';
+import { getFavourites } from '../store/Selectors';
+
+const Favourites = () => {
+
+ const favourites = useStoreState(FavouritesStore, getFavourites);
+
+ const [selectedProduct, setSelectedProduct] = useState([]);
+ const [presentProductModal, dismissProductModal] = useIonModal(ProductModal, {
+
+ dismiss: () => dismissProductModal(),
+ product: selectedProduct
+ });
+
+ const handleProductModal = product => {
+
+ setSelectedProduct(product);
+ presentProductModal();
+ }
+
+ return (
+
+
+
+ Favourites
+
+
+
+
+
+ Favourites
+
+
+
+
+
+ {favourites.map((product, index) => {
+
+ if (product.image !== null && product.image !== "" && !product.image.includes("Placeholder")) {
+ return (
+ handleProductModal(product)} key={index} size="6" sizeXs="6" sizeSm="3" sizeMd="3" sizeXl="2">
+
+
+ {product.title}
+ {product.price}
+
+
+ );
+ } else return null;
+ })}
+
+
+ {favourites.length === 0 &&
+
+
+
+ No favourites yet
+
+
+
+ Add some by clicking the icon on a product
+
+
+
+ }
+
+
+
+ );
+};
+
+export default Favourites;
diff --git a/03_source/mobile/src/pages/DemoReactShopUi/pages/ProductType.jsx b/03_source/mobile/src/pages/DemoReactShopUi/pages/ProductType.jsx
new file mode 100644
index 0000000..4431de5
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactShopUi/pages/ProductType.jsx
@@ -0,0 +1,161 @@
+import { IonBreadcrumb, IonBreadcrumbs, IonButton, IonButtons, IonCol, IonContent, IonGrid, IonHeader, IonIcon, IonImg, IonLabel, IonNote, IonPage, IonRow, IonSearchbar, IonTitle, IonToolbar, useIonModal, useIonRouter } from '@ionic/react';
+import { chevronBack, filter } from 'ionicons/icons';
+import { useRef } from 'react';
+import { useEffect, useState } from 'react';
+import { useParams } from 'react-router';
+import { FilterModal } from '../components/FilterModal';
+import { ProductModal } from '../components/ProductModal';
+import { capitalize, productInfo } from '../utils';
+
+const ProductType = () => {
+
+ const router = useIonRouter();
+ const { category, type } = useParams();
+ const productsRef = useRef();
+
+ const [products, setProducts] = useState([]);
+ const [filteredProducts, setFilteredProducts] = useState([]);
+ const [filterCriteria, setFilterCriteria] = useState("None");
+
+ const filters = productInfo[category].productTypes[type].filters;
+ const searchPlaceholder = productInfo[category].productTypes[type].searchPlaceholder;
+
+ const [selectedProduct, setSelectedProduct] = useState([]);
+ const [presentProductModal, dismissProductModal] = useIonModal(ProductModal, {
+
+ dismiss: () => dismissProductModal(),
+ category,
+ type,
+ product: selectedProduct
+ });
+
+ const handleProductModal = product => {
+
+ setSelectedProduct(product);
+ presentProductModal();
+ }
+
+ const [present, dismiss] = useIonModal(FilterModal, {
+
+ dismiss: () => dismiss(),
+
+ filterCriteria,
+ setFilterCriteria,
+ productsRef,
+ filters
+ });
+
+ useEffect(() => {
+
+ const getProducts = async () => {
+
+ const response = await fetch(`/data/${category}/${type}.json`);
+ const data = await response.json();
+ setProducts(data);
+ setFilteredProducts(data);
+ }
+
+ getProducts();
+ }, [category, type]);
+
+ const openModal = () => {
+
+ present({
+ breakpoints: [0, 0.25],
+ initialBreakpoint: 0.25,
+ backdropBreakpoint: 0
+ });
+ }
+
+ const performSearch = e => {
+
+ const searchCriteria = e.target.value.toLowerCase();
+ let tempFilteredProducts = [...products];
+
+ if (searchCriteria !== "") {
+
+ tempFilteredProducts = tempFilteredProducts.filter(product => product.title.toLowerCase().includes(searchCriteria));
+ setFilteredProducts(tempFilteredProducts);
+ } else {
+
+ setFilteredProducts(products);
+ }
+ }
+
+ return (
+
+
+
+
+
+ router.goBack()}>
+
+ Back
+
+
+ {capitalize(type)}
+
+
+
+
+
+
+ shop
+ {category}
+
+
+
+
+
+
+
+
+
+ {capitalize(category)}
+
+
+ {capitalize(type)}
+
+ {filterCriteria !== "None" &&
+
+
+ {filterCriteria}
+
+ }
+
+
+
+
+
+
+ Filter
+
+
+
+
+ performSearch(e)} />
+
+
+
+ {filteredProducts.map((product, index) => {
+
+ if (product.image !== null && product.image !== "" && !product.image.includes("Placeholder")) {
+ return (
+ handleProductModal(product)} key={index} size="6" sizeXs="6" sizeSm="3" sizeMd="3" sizeXl="2" style={{display: ((filterCriteria !== "None" && product.title.toLowerCase().includes(filterCriteria.toLowerCase())) || filterCriteria === "None") ? "block" : "none"}}>
+
+
+ {product.title}
+ {product.price}
+
+
+ );
+ } else return null;
+ })}
+
+
+
+
+ );
+};
+
+export default ProductType;
\ No newline at end of file
diff --git a/03_source/mobile/src/pages/DemoReactShopUi/pages/index.js b/03_source/mobile/src/pages/DemoReactShopUi/pages/index.js
new file mode 100644
index 0000000..819e56c
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactShopUi/pages/index.js
@@ -0,0 +1,37 @@
+import { heartOutline, homeOutline, shirtOutline } from "ionicons/icons";
+
+import Categories from "./Categories";
+import Favourites from "./Favourites";
+import ProductType from "./ProductType";
+import Category from "./Category";
+
+export const pages = [
+
+ {
+ href: "/categories",
+ icon: shirtOutline,
+ component: Categories,
+ default: true,
+ isTab: true
+ },
+ {
+ href: "/categories/:category/:type",
+ component: ProductType,
+ default: false,
+ isTab: false
+ },
+ {
+ href: "/categories/:category",
+ icon: shirtOutline,
+ component: Category,
+ default: true,
+ isTab: false
+ },
+ {
+ href: "/favourites",
+ icon: heartOutline,
+ component: Favourites,
+ default: false,
+ isTab: true
+ }
+];
\ No newline at end of file
diff --git a/03_source/mobile/src/pages/DemoReactShopUi/store/CartStore.js b/03_source/mobile/src/pages/DemoReactShopUi/store/CartStore.js
new file mode 100644
index 0000000..fb9f018
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactShopUi/store/CartStore.js
@@ -0,0 +1,27 @@
+import { Store } from "pullstate";
+
+const CartStore = new Store({
+
+ cart: []
+});
+
+export default CartStore;
+
+export const addToCart = product => {
+
+ const currentCart = CartStore.getRawState().cart;
+ const added = !currentCart.includes(product);
+
+ CartStore.update(s => {
+
+ if (currentCart.includes(product)) {
+
+ s.cart = currentCart.filter(current => current !== product);
+ } else {
+
+ s.cart = [ ...s.cart, product ];
+ }
+ });
+
+ return added;
+}
\ No newline at end of file
diff --git a/03_source/mobile/src/pages/DemoReactShopUi/store/FavouritesStore.js b/03_source/mobile/src/pages/DemoReactShopUi/store/FavouritesStore.js
new file mode 100644
index 0000000..6ea363d
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactShopUi/store/FavouritesStore.js
@@ -0,0 +1,35 @@
+import { Store } from "pullstate";
+
+const FavouritesStore = new Store({
+
+ favourites: []
+});
+
+export default FavouritesStore;
+
+export const checkIfFavourite = product => {
+
+ const currentFavourites = FavouritesStore.getRawState().favourites;
+ const isFavourite = currentFavourites.includes(product);
+
+ return isFavourite;
+}
+
+export const addToFavourites = (product, category) => {
+
+ const currentFavourites = FavouritesStore.getRawState().favourites;
+ const added = !currentFavourites.includes(product);
+
+ FavouritesStore.update(s => {
+
+ if (!added) {
+
+ s.favourites = currentFavourites.filter(current => current !== product);
+ } else {
+
+ s.favourites = [ ...s.favourites, product ];
+ }
+ });
+
+ return added;
+}
\ No newline at end of file
diff --git a/03_source/mobile/src/pages/DemoReactShopUi/store/Selectors.js b/03_source/mobile/src/pages/DemoReactShopUi/store/Selectors.js
new file mode 100644
index 0000000..960d7df
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactShopUi/store/Selectors.js
@@ -0,0 +1,9 @@
+import { createSelector } from 'reselect';
+
+const getState = state => state;
+
+// General getters
+export const getFavourites = createSelector(getState, state => state.favourites);
+export const checkFavourites = product => createSelector(getState, state => state.favourites.includes(product));
+export const getCart = createSelector(getState, state => state.cart);
+export const getCartCount = createSelector(getState, state => state.cart.length);
\ No newline at end of file
diff --git a/03_source/mobile/src/pages/DemoReactShopUi/store/index.js b/03_source/mobile/src/pages/DemoReactShopUi/store/index.js
new file mode 100644
index 0000000..0be7fa7
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactShopUi/store/index.js
@@ -0,0 +1,2 @@
+export { default as FavouritesStore } from "./FavouritesStore";
+export { default as CartStore } from "./CartStore";
diff --git a/03_source/mobile/src/pages/DemoReactShopUi/theme/variables.css b/03_source/mobile/src/pages/DemoReactShopUi/theme/variables.css
new file mode 100644
index 0000000..07d8b62
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactShopUi/theme/variables.css
@@ -0,0 +1,163 @@
+/* Ionic Variables and Theming. For more info, please see:
+http://ionicframework.com/docs/theming/ */
+
+/** Ionic CSS Variables **/
+:root {
+ /** primary **/
+ --ion-color-primary: #3880ff;
+ --ion-color-primary-rgb: 56, 128, 255;
+ --ion-color-primary-contrast: #ffffff;
+ --ion-color-primary-contrast-rgb: 255, 255, 255;
+ --ion-color-primary-shade: #3171e0;
+ --ion-color-primary-tint: #4c8dff;
+
+ /** secondary **/
+ --ion-color-secondary: #3dc2ff;
+ --ion-color-secondary-rgb: 61, 194, 255;
+ --ion-color-secondary-contrast: #ffffff;
+ --ion-color-secondary-contrast-rgb: 255, 255, 255;
+ --ion-color-secondary-shade: #36abe0;
+ --ion-color-secondary-tint: #50c8ff;
+
+ /** tertiary **/
+ --ion-color-tertiary: #5260ff;
+ --ion-color-tertiary-rgb: 82, 96, 255;
+ --ion-color-tertiary-contrast: #ffffff;
+ --ion-color-tertiary-contrast-rgb: 255, 255, 255;
+ --ion-color-tertiary-shade: #4854e0;
+ --ion-color-tertiary-tint: #6370ff;
+
+ /** success **/
+ --ion-color-success: #2dd36f;
+ --ion-color-success-rgb: 45, 211, 111;
+ --ion-color-success-contrast: #ffffff;
+ --ion-color-success-contrast-rgb: 255, 255, 255;
+ --ion-color-success-shade: #28ba62;
+ --ion-color-success-tint: #42d77d;
+
+ /** warning **/
+ --ion-color-warning: #ffc409;
+ --ion-color-warning-rgb: 255, 196, 9;
+ --ion-color-warning-contrast: #000000;
+ --ion-color-warning-contrast-rgb: 0, 0, 0;
+ --ion-color-warning-shade: #e0ac08;
+ --ion-color-warning-tint: #ffca22;
+
+ /** danger **/
+ --ion-color-danger: #eb445a;
+ --ion-color-danger-rgb: 235, 68, 90;
+ --ion-color-danger-contrast: #ffffff;
+ --ion-color-danger-contrast-rgb: 255, 255, 255;
+ --ion-color-danger-shade: #cf3c4f;
+ --ion-color-danger-tint: #ed576b;
+
+ /** dark **/
+ --ion-color-dark: #222428;
+ --ion-color-dark-rgb: 34, 36, 40;
+ --ion-color-dark-contrast: #ffffff;
+ --ion-color-dark-contrast-rgb: 255, 255, 255;
+ --ion-color-dark-shade: #1e2023;
+ --ion-color-dark-tint: #383a3e;
+
+ /** medium **/
+ --ion-color-medium: #92949c;
+ --ion-color-medium-rgb: 146, 148, 156;
+ --ion-color-medium-contrast: #ffffff;
+ --ion-color-medium-contrast-rgb: 255, 255, 255;
+ --ion-color-medium-shade: #808289;
+ --ion-color-medium-tint: #9d9fa6;
+
+ /** light **/
+ --ion-color-light: #f4f5f8;
+ --ion-color-light-rgb: 244, 245, 248;
+ --ion-color-light-contrast: #000000;
+ --ion-color-light-contrast-rgb: 0, 0, 0;
+ --ion-color-light-shade: #d7d8da;
+ --ion-color-light-tint: #f5f6f9;
+
+ /* CUSTOM */
+ --ion-background-color: white;
+ --ion-tab-bar-background: white;
+ --ion-tab-bar-color: rgb(219, 219, 219);
+ --ion-tab-bar-color-selected: rgb(85, 85, 85);
+}
+
+ion-tab-bar {
+
+ --border: none;
+ height: 5rem;
+}
+
+ion-toolbar,
+ion-header {
+
+ --background: white;
+ --border-color: #F4F5F8;
+}
+
+ion-tab-bar.floating {
+
+--background: white;
+box-shadow: 0px 1px 13px rgba(0, 0, 0, 0.2);
+border-radius: 20px !important;
+
+height: 50px;
+width: 90%;
+padding-top: 5px;
+padding-bottom: 5px;
+
+bottom: 20px;
+position: relative;
+margin: 0 auto !important;
+border-top: none;
+}
+
+ion-tab-button {
+
+ border-radius: 16px !important;
+}
+
+ion-tab-button ion-icon {
+
+font-size: 1.75rem;
+}
+
+.custom-back {
+
+ --color: rgb(99, 99, 99);
+}
+
+
+.page-title {
+
+ text-transform: uppercase;
+}
+
+.tab-dot {
+
+ border-radius: 500px;
+ background-color: var(--ion-tab-bar-color-selected);
+ height: 5px;
+ width: 5px;
+ margin-top: 1.5rem;
+ position: absolute;
+ z-index: 999;
+}
+
+.cart-count {
+
+ position: absolute;
+ background-color: rgb(42, 42, 42);
+ color: white;
+ border-radius: 500px;
+ padding: 5px;
+ width: 20px;
+ height: 20px;
+ font-size: 0.8rem;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ bottom: 2px;
+ margin-left: 2px;
+}
\ No newline at end of file
diff --git a/03_source/mobile/src/pages/DemoReactShopUi/utils/index.js b/03_source/mobile/src/pages/DemoReactShopUi/utils/index.js
new file mode 100644
index 0000000..6d26701
--- /dev/null
+++ b/03_source/mobile/src/pages/DemoReactShopUi/utils/index.js
@@ -0,0 +1,157 @@
+export const capitalize = s => s && (s[0].toUpperCase() + s.slice(1)).replaceAll("_", " ");
+
+export const productInfo = {
+
+ men: {
+
+ coverImage: "/assets/men.jpeg",
+ productTypes: {
+
+ formal_shirts: {
+ coverImage: "/assets/formal_shirts2.jpeg",
+ filters: ["None", "Regular", "Slim", "Stretch"],
+ searchPlaceholder: "Single Cuff"
+ },
+ sportswear: {
+ coverImage: "/assets/sportswear2.jpeg",
+ filters: ["None", "Trainers", "Joggers", "Shorts", "Hoodie"],
+ searchPlaceholder: "Nike"
+ },
+ coats: {
+ coverImage: "/assets/coats3.jpeg",
+ filters: ["None", "Funnel", "Hooded", "Barbour", "Collar"],
+ searchPlaceholder: "Bomber"
+ }
+ }
+ },
+ women: {
+
+ coverImage: "/assets/women.jpeg",
+ productTypes: {
+
+ jeans: {
+ coverImage: "/assets/jeans.jpeg",
+ filters: ["None", "Skinny", "Slim", "Boot Cut", "Flare"],
+ searchPlaceholder: "Skinny"
+ },
+ dresses: {
+ coverImage: "/assets/dresses3.jpeg",
+ filters: ["None", "Short", "Maxi", "Long", "Regular"],
+ searchPlaceholder: "Long Sleeve"
+ },
+ makeup: {
+ coverImage: "/assets/makeup2.jpeg",
+ filters: ["None", "Mascara", "Lip Gloss", "Foundation", "Blush"],
+ searchPlaceholder: "Brush Set"
+ }
+ }
+ },
+ home: {
+
+ coverImage: "/assets/home.jpeg",
+ productTypes: {
+
+ beds: {
+ coverImage: "/assets/beds.jpeg",
+ filters: ["None", "Metal", "Ottoman", "Storage", "Wooden"],
+ searchPlaceholder: "Upholstered"
+ },
+ office: {
+ coverImage: "/assets/office.jpeg",
+ filters: ["None", "Desk", "Chair", "Lamp", "Shelf"],
+ searchPlaceholder: "Space Saving"
+ },
+ coffee_tables: {
+ coverImage: "/assets/coffee_table.jpeg",
+ filters: ["None", "Wood", "Glass", "Round", "Storage"],
+ searchPlaceholder: "Oak Effect"
+ }
+ }
+ },
+
+};
+
+export const productSpecs = {
+
+ dimensions: {
+
+ header: "Dimensions",
+ options: [
+ {
+ label: "Height",
+ value: "100cm"
+ },
+ {
+ label: "Width",
+ value: "130cm"
+ },
+ {
+ label: "Depth",
+ value: "150cm"
+ }
+ ]
+ },
+ shipping: {
+
+ header: "Shipping",
+ options: [
+ {
+ label: "UK",
+ value: "£4.99"
+ },
+ {
+ label: "USA",
+ value: "£6.99"
+ },
+ {
+ label: "Gloal",
+ value: "£9.99"
+ }
+ ]
+ },
+ colors: {
+
+ header: "Colors",
+ noteColor: true,
+ options: [
+ {
+ label: "Red",
+ value: true
+ },
+ {
+ label: "Blue",
+ value: false
+ },
+ {
+ label: "Brown",
+ value: true
+ }
+ ]
+ },
+ sizes: {
+
+ header: "Sizes",
+ wrapText: true,
+ options: [
+ {
+ label: "Large",
+ value: "Check size guide for details"
+ },
+ {
+ label: "Width",
+ value: "Check size guide for details"
+ },
+ {
+ label: "Depth",
+ value: "Check size guide for details"
+ }
+ ]
+ }
+};
+
+export const randomCount = () => {
+
+ const max = 273;
+ const min = 23;
+ return Math.floor(Math.random() * (max - min) + min).toFixed(0);
+}
\ No newline at end of file