diff --git a/03_source/mobile/src/pages/DemoEcommerceExample/components/ProductCard.module.css b/03_source/mobile/src/pages/DemoEcommerceExample/components/ProductCard.module.css new file mode 100644 index 0000000..7cb75fd --- /dev/null +++ b/03_source/mobile/src/pages/DemoEcommerceExample/components/ProductCard.module.css @@ -0,0 +1,72 @@ +.categoryPage ion-toolbar { + + --border-style: none; +} + +.categoryCard { + + display: flex; + flex-direction: column; + justify-content: center; + align-content: center; + align-items: center; + /* min-height: 20rem !important; */ +} + +.productCardActions { + + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; + margin-bottom: 1rem; +} + +.productCardAction { + + font-size: 1.1rem; +} + +.productCardHeader { + + min-height: 17rem; +} + +.productCardHeader p { + + font-size: 0.8rem; + padding: 0; + margin: 0; + margin-top: 0.75rem; +} + +.categoryCardContent { + + display: flex; + flex-direction: column; +} + +.categoryCardContent ion-button { + + height: 1.5rem; + font-size: 0.8rem; +} + +.categoryCardContent p { + + font-size: 0.8rem; + padding: 0; + margin: 0; +} + +.categoryCard img { + + /* border-radius: 5px; */ + /* padding: 1rem; */ +} + +.productPrice { + + display: flex; + flex-direction: row; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoEcommerceExample/components/ProductCard.tsx b/03_source/mobile/src/pages/DemoEcommerceExample/components/ProductCard.tsx new file mode 100644 index 0000000..e3a0266 --- /dev/null +++ b/03_source/mobile/src/pages/DemoEcommerceExample/components/ProductCard.tsx @@ -0,0 +1,105 @@ +import { IonButton, IonCard, IonCardContent, IonCardHeader, IonCol, IonIcon } from '@ionic/react'; +import { arrowRedoOutline, cart, cartOutline, heart, heartOutline } from 'ionicons/icons'; +import { useEffect, useRef, useState } from 'react'; +import { addToCart } from '../data/CartStore'; +import { addToFavourites, FavouritesStore } from '../data/FavouritesStore'; +import styles from './ProductCard.module.css'; + +const ProductCard = (props): React.FC => { + const { product, category, index, cartRef } = props; + const favourites = FavouritesStore.useState((s) => s.product_ids); + + const productCartRef = useRef(); + const productFavouriteRef = useRef(); + const [isFavourite, setIsFavourite] = useState(false); + + useEffect(() => { + const tempIsFavourite = favourites.find((f) => f === `${category.slug}/${product.id}`); + setIsFavourite(tempIsFavourite ? true : false); + }, [props.product, favourites]); + + const addProductToFavourites = (e, categorySlug, productID) => { + e.preventDefault(); + e.stopPropagation(); + addToFavourites(categorySlug, productID); + + productFavouriteRef.current.style.display = ''; + productFavouriteRef.current.classList.add('animate__fadeOutTopRight'); + + setTimeout(() => { + if (productCartRef.current) { + productFavouriteRef.current.classList.remove('animate__fadeOutTopRight'); + productFavouriteRef.current.style.display = 'none'; + } + }, 500); + }; + + const addProductToCart = (e, categorySlug, productID) => { + e.preventDefault(); + e.stopPropagation(); + + productCartRef.current.style.display = ''; + productCartRef.current.classList.add('animate__fadeOutUp'); + + setTimeout(() => { + cartRef.current.classList.add('animate__tada'); + addToCart(categorySlug, productID); + + setTimeout(() => { + cartRef.current.classList.remove('animate__tada'); + productCartRef.current.style.display = 'none'; + }, 500); + }, 500); + }; + + return ( + + + +
+ addProductToFavourites(e, category.slug, product.id)} + /> + + +
+ product pic +

{product.name}

+
+ + +
+ + {product.price} + + addProductToCart(e, category.slug, product.id)}> + + + + +
+
+
+
+ ); +}; + +export default ProductCard; diff --git a/03_source/mobile/src/pages/DemoEcommerceExample/data/CartStore.ts b/03_source/mobile/src/pages/DemoEcommerceExample/data/CartStore.ts new file mode 100644 index 0000000..c9af57b --- /dev/null +++ b/03_source/mobile/src/pages/DemoEcommerceExample/data/CartStore.ts @@ -0,0 +1,18 @@ +import { Store } from 'pullstate'; + +export const CartStore = new Store({ + total: 0, + product_ids: [], +}); + +export const addToCart = (categorySlug, productID) => { + CartStore.update((s) => { + s.product_ids = [...s.product_ids, `${categorySlug}/${parseInt(productID)}`]; + }); +}; + +export const removeFromCart = (productIndex) => { + CartStore.update((s) => { + s.product_ids.splice(productIndex, 1); + }); +}; diff --git a/03_source/mobile/src/pages/DemoEcommerceExample/data/FavouritesStore.ts b/03_source/mobile/src/pages/DemoEcommerceExample/data/FavouritesStore.ts new file mode 100644 index 0000000..53d6d22 --- /dev/null +++ b/03_source/mobile/src/pages/DemoEcommerceExample/data/FavouritesStore.ts @@ -0,0 +1,17 @@ +import { Store } from "pullstate"; + +export const FavouritesStore = new Store({ + + total: 0, + product_ids: [] +}); + +export const addToFavourites = (categorySlug, productID) => { + FavouritesStore.update(s => { + if (s.product_ids.find(id => id === `${ categorySlug }/${ parseInt(productID) }`)) { + s.product_ids = s.product_ids.filter(id => id !== `${ categorySlug }/${ parseInt(productID) }`); + } else { + s.product_ids = [ ...s.product_ids, `${ categorySlug }/${ parseInt(productID) }` ]; + } + }); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoEcommerceExample/data/ProductStore.ts b/03_source/mobile/src/pages/DemoEcommerceExample/data/ProductStore.ts new file mode 100644 index 0000000..86dbaf4 --- /dev/null +++ b/03_source/mobile/src/pages/DemoEcommerceExample/data/ProductStore.ts @@ -0,0 +1,6 @@ +import { Store } from "pullstate"; + +export const ProductStore = new Store({ + + products: [] +}); \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoEcommerceExample/data/fetcher.ts b/03_source/mobile/src/pages/DemoEcommerceExample/data/fetcher.ts new file mode 100644 index 0000000..4d026ff --- /dev/null +++ b/03_source/mobile/src/pages/DemoEcommerceExample/data/fetcher.ts @@ -0,0 +1,57 @@ +import { ProductStore } from './ProductStore'; + +export const fetchData = async () => { + const json = [ + 'beds.json', + 'armchairs.json', + 'coffee_tables.json', + 'cushions.json', + 'floor_lamps.json', + 'office_chairs.json', + ]; + + var products = []; + + json.forEach(async (category) => { + const products = await fetchProducts(category); + + let categoryName = category.replace('.json', ''); + categoryName = categoryName.replace('_', ' '); + categoryName = uppercaseWords(categoryName); + + const productCategory = { + name: categoryName, + slug: category.replace('.json', ''), + cover: products[6].image, + products, + }; + + ProductStore.update((s) => { + s.products = [...s.products, productCategory]; + }); + }); + + return products; +}; + +const fetchProducts = async (category) => { + const response = await fetch(`products/${category}`); + const data = await response.json(); + + // Set a product id + await data.forEach((d, i) => { + d.id = i + 1; + }); + + return data; +}; + +const uppercaseWords = (words) => { + words = words + .toLowerCase() + .split(' ') + .map((s) => s.charAt(0).toUpperCase() + s.substring(1)) + .join(' '); + + return words; +}; diff --git a/03_source/mobile/src/pages/DemoEcommerceExample/index.tsx b/03_source/mobile/src/pages/DemoEcommerceExample/index.tsx index 543bf33..f351a32 100644 --- a/03_source/mobile/src/pages/DemoEcommerceExample/index.tsx +++ b/03_source/mobile/src/pages/DemoEcommerceExample/index.tsx @@ -3,23 +3,48 @@ import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } import { cloudOutline, searchOutline } from 'ionicons/icons'; import { Route, Redirect } from 'react-router'; -import Tab1 from './AppPages/Tab1'; -import Tab2 from './AppPages/Tab2'; +// import Tab1 from './AppPages/Tab1'; +// import Tab2 from './AppPages/Tab2'; + +import Home from './pages/Home'; +import { fetchData } from './data/fetcher'; +import CategoryProducts from './pages/CategoryProducts'; +import Product from './pages/Product'; +import FavouriteProducts from './pages/FavouriteProducts'; +import CartProducts from './pages/CartProducts'; import './style.scss'; +import React, { useEffect } from 'react'; + +function DemoEcommerceExample(): React.JSX.Element { + useEffect(() => { + fetchData(); + }, []); -function DemoEcommerceExample() { return ( - - - - - + + - + + + + + + + + + + + + + + + + + {/* */} diff --git a/03_source/mobile/src/pages/DemoEcommerceExample/module.d.ts b/03_source/mobile/src/pages/DemoEcommerceExample/module.d.ts new file mode 100644 index 0000000..d774364 --- /dev/null +++ b/03_source/mobile/src/pages/DemoEcommerceExample/module.d.ts @@ -0,0 +1,4 @@ +declare module '*.module.css' { + const classes: { readonly [key: string]: string }; + export default classes; +} diff --git a/03_source/mobile/src/pages/DemoEcommerceExample/pages/CartProducts.module.css b/03_source/mobile/src/pages/DemoEcommerceExample/pages/CartProducts.module.css new file mode 100644 index 0000000..6be2867 --- /dev/null +++ b/03_source/mobile/src/pages/DemoEcommerceExample/pages/CartProducts.module.css @@ -0,0 +1,31 @@ +.cartCheckout { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + align-content: center; + margin: 1rem; +} + +.cartFooter { + border-top: 2px solid rgb(200, 200, 200); + background-color: white; +} + +.cartCheckout ion-card-subtitle { + font-size: 1.3rem; +} + +.cartItem ion-avatar { + height: 4rem; + width: 4rem; +} + +.cartSlider:not(:nth-child(1)) { + border-top: 2px solid rgb(236, 236, 236); +} + +.cartActions { + display: flex; + flex-direction: column; +} diff --git a/03_source/mobile/src/pages/DemoEcommerceExample/pages/CartProducts.tsx b/03_source/mobile/src/pages/DemoEcommerceExample/pages/CartProducts.tsx new file mode 100644 index 0000000..208eb31 --- /dev/null +++ b/03_source/mobile/src/pages/DemoEcommerceExample/pages/CartProducts.tsx @@ -0,0 +1,161 @@ +import { + IonAvatar, + IonBadge, + IonButton, + IonButtons, + IonCardSubtitle, + IonCol, + IonContent, + IonFooter, + IonHeader, + IonIcon, + IonImg, + IonItem, + IonItemOption, + IonItemOptions, + IonItemSliding, + IonLabel, + IonList, + IonNote, + IonPage, + IonRow, + IonTitle, + IonToolbar, +} from '@ionic/react'; +import { cart, checkmarkSharp, chevronBackOutline, trashOutline } from 'ionicons/icons'; +import React, { useEffect, useRef, useState } from 'react'; +import { CartStore, removeFromCart } from '../data/CartStore'; +import { ProductStore } from '../data/ProductStore'; + +import styles from './CartProducts.module.css'; + +const CartProducts = (): React.JSX.Element => { + const cartRef = useRef(null); + const products = ProductStore.useState((s) => s.products); + const shopCart = CartStore.useState((s) => s.product_ids); + const [cartProducts, setCartProducts] = useState([]); + const [amountLoaded, setAmountLoaded] = useState(6); + + const [total, setTotal] = useState(0); + + useEffect(() => { + const getCartProducts = () => { + setCartProducts([]); + setTotal(0); + + shopCart.forEach((product) => { + var favouriteParts = product.split('/'); + var categorySlug = favouriteParts[0]; + var productID = favouriteParts[1]; + + const tempCategory = products.filter((p) => p.slug === categorySlug)[0]; + const tempProduct = tempCategory.products.filter( + (p) => parseInt(p.id) === parseInt(productID) + )[0]; + + const tempCartProduct = { + category: tempCategory, + product: tempProduct, + }; + + setTotal((prevTotal) => prevTotal + parseInt(tempProduct.price.replace('£', ''))); + setCartProducts((prevSearchResults) => [...prevSearchResults, tempCartProduct]); + }); + }; + + getCartProducts(); + }, [shopCart]); + + const fetchMore = async (e) => { + // Increment the amount loaded by 6 for the next iteration + setAmountLoaded((prevAmount) => prevAmount + 6); + e.target.complete(); + }; + + const removeProductFromCart = async (index) => { + removeFromCart(index); + }; + + return ( + + + + + + +  Categories + + + Cart + + + {shopCart.length} + + + + + + + + + + + + {cartProducts && cartProducts.length}{' '} + {cartProducts.length > 1 || cartProducts.length === 0 ? ' products' : ' product'}{' '} + found + + + + + + {cartProducts && + cartProducts.map((product, index) => { + if (index <= amountLoaded) { + return ( + + + + + + +

{product.category.name}

+

{product.product.name}

+
+ +
+ {product.product.price} +
+
+ + + removeProductFromCart(index)} + > + + + +
+ ); + } + })} +
+
+ + +
+ £{total.toFixed(2)} + + + +  Checkout + +
+
+
+ ); +}; + +export default CartProducts; diff --git a/03_source/mobile/src/pages/DemoEcommerceExample/pages/CategoryProducts.module.css b/03_source/mobile/src/pages/DemoEcommerceExample/pages/CategoryProducts.module.css new file mode 100644 index 0000000..e42a0b1 --- /dev/null +++ b/03_source/mobile/src/pages/DemoEcommerceExample/pages/CategoryProducts.module.css @@ -0,0 +1,10 @@ +.categoryPage ion-toolbar { + + --border-style: none; +} + +.search { + + --background: rgb(240, 240, 240); + --color: black; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoEcommerceExample/pages/CategoryProducts.tsx b/03_source/mobile/src/pages/DemoEcommerceExample/pages/CategoryProducts.tsx new file mode 100644 index 0000000..ccbd65a --- /dev/null +++ b/03_source/mobile/src/pages/DemoEcommerceExample/pages/CategoryProducts.tsx @@ -0,0 +1,134 @@ +import { + IonBadge, + IonButton, + IonButtons, + IonCol, + IonContent, + IonGrid, + IonHeader, + IonIcon, + IonInfiniteScroll, + IonInfiniteScrollContent, + IonNote, + IonPage, + IonRow, + IonSearchbar, + IonTitle, + IonToolbar, +} from '@ionic/react'; +import { cart, chevronBackOutline, searchOutline } from 'ionicons/icons'; +import { useEffect, useRef, useState } from 'react'; +import { useParams } from 'react-router'; +import ProductCard from '../components/ProductCard'; + +import { CartStore } from '../data/CartStore'; +import { ProductStore } from '../data/ProductStore'; + +import styles from './CategoryProducts.module.css'; + +const CategoryProducts = () => { + const params = useParams(); + const cartRef = useRef(); + const products = ProductStore.useState((s) => s.products); + const shopCart = CartStore.useState((s) => s.product_ids); + const [category, setCategory] = useState({}); + const [searchResults, setsearchResults] = useState([]); + const [amountLoaded, setAmountLoaded] = useState(6); + + useEffect(() => { + const categorySlug = params.slug; + const tempCategory = products.filter((p) => p.slug === categorySlug)[0]; + setCategory(tempCategory); + setsearchResults(tempCategory.products); + }, [params.slug]); + + const fetchMore = async (e) => { + // Increment the amount loaded by 6 for the next iteration + setAmountLoaded((prevAmount) => prevAmount + 6); + e.target.complete(); + }; + + const search = async (e) => { + const searchVal = e.target.value; + + if (searchVal !== '') { + const tempResults = category.products.filter((p) => + p.name.toLowerCase().includes(searchVal.toLowerCase()) + ); + setsearchResults(tempResults); + } else { + setsearchResults(category.products); + } + }; + + return ( + + + + + + +  Categories + + + {category && category.name} + + + {shopCart.length} + + + + + + + + + + + + + + + {searchResults && searchResults.length}{' '} + {searchResults.length > 1 || searchResults.length === 0 ? ' products' : ' product'}{' '} + found + + + + + + {searchResults && + searchResults.map((product, index) => { + if (index <= amountLoaded && product.image) { + return ( + + ); + } + })} + + + + + + + + + ); +}; + +export default CategoryProducts; diff --git a/03_source/mobile/src/pages/DemoEcommerceExample/pages/FavouriteProducts.tsx b/03_source/mobile/src/pages/DemoEcommerceExample/pages/FavouriteProducts.tsx new file mode 100644 index 0000000..7543053 --- /dev/null +++ b/03_source/mobile/src/pages/DemoEcommerceExample/pages/FavouriteProducts.tsx @@ -0,0 +1,131 @@ +import { + IonBadge, + IonButton, + IonButtons, + IonCol, + IonContent, + IonGrid, + IonHeader, + IonIcon, + IonInfiniteScroll, + IonInfiniteScrollContent, + IonNote, + IonPage, + IonRow, + IonTitle, + IonToolbar, +} from '@ionic/react'; +import { cart, chevronBackOutline } from 'ionicons/icons'; +import { useEffect, useRef, useState } from 'react'; +import ProductCard from '../components/ProductCard'; +import { CartStore } from '../data/CartStore'; +import { FavouritesStore } from '../data/FavouritesStore'; +import { ProductStore } from '../data/ProductStore'; + +import styles from './CategoryProducts.module.css'; + +const FavouriteProducts = () => { + const cartRef = useRef(null); + const products = ProductStore.useState((s) => s.products); + const favourites = FavouritesStore.useState((s) => s.product_ids); + const shopCart = CartStore.useState((s) => s.product_ids); + const [searchResults, setSearchResults] = useState([]); + const [amountLoaded, setAmountLoaded] = useState(6); + + useEffect(() => { + const getFavourites = () => { + setSearchResults([]); + + favourites.forEach((favourite) => { + var favouriteParts = favourite.split('/'); + var categorySlug = favouriteParts[0]; + var productID = favouriteParts[1]; + + const tempCategory = products.filter((p) => p.slug === categorySlug)[0]; + const tempProduct = tempCategory.products.filter( + (p) => parseInt(p.id) === parseInt(productID) + )[0]; + + const tempFavourite = { + category: tempCategory, + product: tempProduct, + }; + + setSearchResults((prevSearchResults) => [...prevSearchResults, tempFavourite]); + }); + }; + + getFavourites(); + }, [favourites]); + + const fetchMore = async (e) => { + // Increment the amount loaded by 6 for the next iteration + setAmountLoaded((prevAmount) => prevAmount + 6); + e.target.complete(); + }; + + return ( + + + + + + +  Categories + + + Favourites + + + {shopCart.length} + + + + + + + + + + + + + {searchResults && searchResults.length}{' '} + {searchResults.length > 1 || searchResults.length === 0 + ? ' favourites' + : ' favourite'}{' '} + found + + + + + + {searchResults && + searchResults.map((product, index) => { + if (index <= amountLoaded) { + return ( + + ); + } + })} + + + + + + + + + ); +}; + +export default FavouriteProducts; diff --git a/03_source/mobile/src/pages/DemoEcommerceExample/pages/Home.module.css b/03_source/mobile/src/pages/DemoEcommerceExample/pages/Home.module.css new file mode 100644 index 0000000..e543907 --- /dev/null +++ b/03_source/mobile/src/pages/DemoEcommerceExample/pages/Home.module.css @@ -0,0 +1,42 @@ +.homePage ion-toolbar { + + --border-style: none; +} + +.logo { + + margin-top: 0.25rem; + color: var(--ion-color-primary); +} + +.categoryCard, +.categoryCardContent { + + display: flex; + flex-direction: column; + justify-content: center; + align-content: center; + align-items: center; +} + +.categoryCardContent ion-button { + + height: 1.5rem; + font-size: 0.8rem; +} + +.categoryCardContent { + + background-color: rgb(238, 238, 238); +} + +.categoryCardContent ion-card-subtitle { + + /* color: rgb(78, 78, 78); */ +} + +.categoryCard img { + + /* border-radius: 5px; */ + padding: 1rem; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoEcommerceExample/pages/Home.tsx b/03_source/mobile/src/pages/DemoEcommerceExample/pages/Home.tsx new file mode 100644 index 0000000..25cb577 --- /dev/null +++ b/03_source/mobile/src/pages/DemoEcommerceExample/pages/Home.tsx @@ -0,0 +1,88 @@ +import { useState } from 'react'; +import { + IonBadge, + IonButton, + IonButtons, + IonCard, + IonCardContent, + IonCardSubtitle, + IonCol, + IonContent, + IonGrid, + IonHeader, + IonIcon, + IonPage, + IonRow, + IonTitle, + IonToolbar, +} from '@ionic/react'; + +import styles from './Home.module.css'; +import { cart, heart } from 'ionicons/icons'; + +import { ProductStore } from '../data/ProductStore'; +import { FavouritesStore } from '../data/FavouritesStore'; +import { CartStore } from '../data/CartStore'; + +const Home = () => { + const products = ProductStore.useState((s) => s.products); + const favourites = FavouritesStore.useState((s) => s.product_ids); + const shopCart = CartStore.useState((s) => s.product_ids); + + return ( + + + + Categories + + + Ionic Furniture + + + + {favourites.length} + + + + + {shopCart.length} + + + + + + + + + + + Categories + + + + + + {products.map((category, index) => { + return ( + + + category cover + + + {category.name} + + + + ); + })} + + + + + ); +}; + +export default Home; diff --git a/03_source/mobile/src/pages/DemoEcommerceExample/pages/Product.module.css b/03_source/mobile/src/pages/DemoEcommerceExample/pages/Product.module.css new file mode 100644 index 0000000..253b7ed --- /dev/null +++ b/03_source/mobile/src/pages/DemoEcommerceExample/pages/Product.module.css @@ -0,0 +1,66 @@ +.categoryPage ion-toolbar { + + --border-style: none; +} + +.categoryCard { + + display: flex; + flex-direction: column; + justify-content: center; + align-content: center; + align-items: center; + text-align: center; +} + +.productCardActions { + + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; + margin-bottom: 1rem; +} + +.productCardAction { + + font-size: 1.1rem; +} + +.productCardHeader { + + min-height: 17rem; +} + +.productCardHeader p { + + font-size: 1.2rem; + padding: 0; + margin: 0; + margin-top: 0.75rem; +} + +.categoryCardContent { + + display: flex; + flex-direction: column; + text-align: center; +} + +.categoryCardContent ion-button { + + font-size: 0.8rem; +} + +.categoryCardContent p { + + font-size: 1.5rem; + padding: 0; + margin: 0; +} + +.productPrice { + + display: flex; + flex-direction: row; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoEcommerceExample/pages/Product.tsx b/03_source/mobile/src/pages/DemoEcommerceExample/pages/Product.tsx new file mode 100644 index 0000000..c7c7f18 --- /dev/null +++ b/03_source/mobile/src/pages/DemoEcommerceExample/pages/Product.tsx @@ -0,0 +1,210 @@ +import { + IonBadge, + IonButton, + IonButtons, + IonCard, + IonCardContent, + IonCardHeader, + IonCardSubtitle, + IonCol, + IonContent, + IonGrid, + IonHeader, + IonIcon, + IonPage, + IonRow, + IonTitle, + IonToolbar, +} from '@ionic/react'; +import { + arrowRedoOutline, + cart, + cartOutline, + chevronBackOutline, + heart, + heartOutline, +} from 'ionicons/icons'; +import { useEffect, useRef, useState } from 'react'; +import { useParams } from 'react-router'; +import ProductCard from '../components/ProductCard'; +import { addToCart, CartStore } from '../data/CartStore'; +import { addToFavourites, FavouritesStore } from '../data/FavouritesStore'; +import { ProductStore } from '../data/ProductStore'; + +import styles from './Product.module.css'; + +const Product = () => { + const params = useParams(); + const cartRef = useRef(null); + const products = ProductStore.useState((s) => s.products); + const favourites = FavouritesStore.useState((s) => s.product_ids); + const [isFavourite, setIsFavourite] = useState(false); + const shopCart = CartStore.useState((s) => s.product_ids); + const [product, setProduct] = useState({}); + const [category, setCategory] = useState({}); + + useEffect(() => { + const categorySlug = params.slug; + const productID = params.id; + const tempCategory = products.filter((p) => p.slug === categorySlug)[0]; + const tempProduct = tempCategory.products.filter( + (p) => parseInt(p.id) === parseInt(productID) + )[0]; + + const tempIsFavourite = favourites.find((f) => f === `${categorySlug}/${productID}`); + + setIsFavourite(tempIsFavourite); + setCategory(tempCategory); + setProduct(tempProduct); + }, [params.slug, params.id]); + + useEffect(() => { + const tempIsFavourite = favourites.find((f) => f === `${category.slug}/${product.id}`); + setIsFavourite(tempIsFavourite ? true : false); + }, [favourites, product]); + + const addProductToFavourites = (e, categorySlug, productID) => { + e.preventDefault(); + addToFavourites(categorySlug, productID); + + document.getElementById( + `placeholder_favourite_product_${categorySlug}_${productID}` + ).style.display = ''; + document + .getElementById(`placeholder_favourite_product_${categorySlug}_${productID}`) + .classList.add('animate__fadeOutTopRight'); + }; + + const addProductToCart = (e, categorySlug, productID) => { + e.preventDefault(); + + document.getElementById(`placeholder_cart_${categorySlug}_${productID}`).style.display = ''; + document + .getElementById(`placeholder_cart_${categorySlug}_${productID}`) + .classList.add('animate__fadeOutUp'); + + setTimeout(() => { + cartRef.current.classList.add('animate__tada'); + addToCart(categorySlug, productID); + + setTimeout(() => { + cartRef.current.classList.remove('animate__tada'); + }, 500); + }, 500); + }; + + return ( + + + + + + +  {category.name} + + + + View Product + + + {shopCart.length} + + + + + + + + + + + + + +
+ addProductToFavourites(e, category.slug, product.id)} + /> + + +
+ product pic +

{product.name}

+
+ + +
+ + {product.price} + + addProductToCart(e, category.slug, product.id)} + > + +   Add to Cart + + + +
+
+
+
+
+ + + + Similar products... + + + + + {category && + category.products && + category.products.map((similar, index) => { + if (similar.id !== product.id && product.image && index < 4) { + return ( + + ); + } + })} + +
+
+
+ ); +}; + +export default Product; diff --git a/03_source/mobile/src/pages/DemoEcommerceExample/theme/variables.css b/03_source/mobile/src/pages/DemoEcommerceExample/theme/variables.css new file mode 100644 index 0000000..985de6f --- /dev/null +++ b/03_source/mobile/src/pages/DemoEcommerceExample/theme/variables.css @@ -0,0 +1,81 @@ +/* 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; + + --ion-toolbar-color: black; + --ion-grid-column-padding: 0rem; + /* --ion-toolbar-background: var(--ion-color-warning); */ +} \ No newline at end of file