From 766720e07500c280c2059ae332c6cdf6984201a8 Mon Sep 17 00:00:00 2001 From: louiscklaw Date: Sun, 8 Jun 2025 19:07:48 +0800 Subject: [PATCH] update demo-react-shop-ui, --- .../components/AddToCartButton.jsx | 58 +++++++ .../components/Breadcrumbs.jsx | 23 +++ .../DemoReactShopUi/components/CartModal.jsx | 82 +++++++++ .../components/FilterModal.jsx | 36 ++++ .../components/ProductModal.css | 88 ++++++++++ .../components/ProductModal.jsx | 76 ++++++++ .../components/ProductReviews.jsx | 23 +++ .../ProductSpecificationsAccordion.jsx | 58 +++++++ .../DemoReactShopUi/pages/Categories.jsx | 45 +++++ .../pages/Categories.module.scss | 20 +++ .../pages/DemoReactShopUi/pages/Category.jsx | 54 ++++++ .../DemoReactShopUi/pages/Favourites.jsx | 77 +++++++++ .../DemoReactShopUi/pages/ProductType.jsx | 161 +++++++++++++++++ .../src/pages/DemoReactShopUi/pages/index.js | 37 ++++ .../pages/DemoReactShopUi/store/CartStore.js | 27 +++ .../DemoReactShopUi/store/FavouritesStore.js | 35 ++++ .../pages/DemoReactShopUi/store/Selectors.js | 9 + .../src/pages/DemoReactShopUi/store/index.js | 2 + .../pages/DemoReactShopUi/theme/variables.css | 163 ++++++++++++++++++ .../src/pages/DemoReactShopUi/utils/index.js | 157 +++++++++++++++++ 20 files changed, 1231 insertions(+) create mode 100644 03_source/mobile/src/pages/DemoReactShopUi/components/AddToCartButton.jsx create mode 100644 03_source/mobile/src/pages/DemoReactShopUi/components/Breadcrumbs.jsx create mode 100644 03_source/mobile/src/pages/DemoReactShopUi/components/CartModal.jsx create mode 100644 03_source/mobile/src/pages/DemoReactShopUi/components/FilterModal.jsx create mode 100644 03_source/mobile/src/pages/DemoReactShopUi/components/ProductModal.css create mode 100644 03_source/mobile/src/pages/DemoReactShopUi/components/ProductModal.jsx create mode 100644 03_source/mobile/src/pages/DemoReactShopUi/components/ProductReviews.jsx create mode 100644 03_source/mobile/src/pages/DemoReactShopUi/components/ProductSpecificationsAccordion.jsx create mode 100644 03_source/mobile/src/pages/DemoReactShopUi/pages/Categories.jsx create mode 100644 03_source/mobile/src/pages/DemoReactShopUi/pages/Categories.module.scss create mode 100644 03_source/mobile/src/pages/DemoReactShopUi/pages/Category.jsx create mode 100644 03_source/mobile/src/pages/DemoReactShopUi/pages/Favourites.jsx create mode 100644 03_source/mobile/src/pages/DemoReactShopUi/pages/ProductType.jsx create mode 100644 03_source/mobile/src/pages/DemoReactShopUi/pages/index.js create mode 100644 03_source/mobile/src/pages/DemoReactShopUi/store/CartStore.js create mode 100644 03_source/mobile/src/pages/DemoReactShopUi/store/FavouritesStore.js create mode 100644 03_source/mobile/src/pages/DemoReactShopUi/store/Selectors.js create mode 100644 03_source/mobile/src/pages/DemoReactShopUi/store/index.js create mode 100644 03_source/mobile/src/pages/DemoReactShopUi/theme/variables.css create mode 100644 03_source/mobile/src/pages/DemoReactShopUi/utils/index.js 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 + + +

{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 => ( + + +
+ cover +

{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 => ( + + +
+ cover +

{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