update demo-react-shop-ui,

This commit is contained in:
louiscklaw
2025-06-08 19:07:48 +08:00
parent 19af60c410
commit 766720e075
20 changed files with 1231 additions and 0 deletions

View File

@@ -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 (
<IonButton color="dark" expand="full" onClick={() => handleAddToCart(product)}>
<IonIcon icon={cartOutline} />&nbsp;
Add to Cart
<CreateAnimation ref={animationRef} {...mainAnimation}>
<IonIcon icon={cartOutline} size="large" style={floatStyle} />
</CreateAnimation>
</IonButton>
);
}

View File

@@ -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 (
<IonBreadcrumbs maxItems={maxItems} onIonCollapsedClick={handleClick}>
<IonBreadcrumb color="medium">Page 1</IonBreadcrumb>
<IonBreadcrumb color="medium">Page 2</IonBreadcrumb>
<IonBreadcrumb color="medium">Page 3</IonBreadcrumb>
<IonBreadcrumb>Page 4</IonBreadcrumb>
</IonBreadcrumbs>
);
}

View File

@@ -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 (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Cart</IonTitle>
<IonButtons slot="end" onClick={props.close}>
<IonIcon icon={close} size="large" />
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent>
<IonGrid>
<IonRow style={{borderBottom: "1px solid black"}} className="ion-margin-bottom">
<IonItem lines="none">
<IonLabel>
<h1>{cart.length} products in your cart</h1>
<IonText color="medium">
<h2>Review products and checkout</h2>
</IonText>
</IonLabel>
</IonItem>
</IonRow>
</IonGrid>
{cart.map((item, index) => (
<IonItemSliding>
<IonItem key={index} lines="none" className="ion-padding-end" style={{paddingTop: "0.75rem", paddingBottom: "0.75rem"}}>
<IonThumbnail>
<img src={item.image} alt="item" />
</IonThumbnail>
<IonLabel className="ion-padding-start ion-text-wrap">
<h2>{item.title}</h2>
<p>{item.price}</p>
</IonLabel>
</IonItem>
<IonItemOptions side="end">
<IonItemOption color="danger" onClick={() => addToCart(item)}>
Remove
</IonItemOption>
</IonItemOptions>
</IonItemSliding>
))}
</IonContent>
<IonFooter className="ion-padding-bottom ion-padding-start ion-padding-end" style={{paddingBottom: "3rem"}}>
<IonRow className="ion-justify-content-between">
<IonCol size="8">
<h1>Total</h1>
</IonCol>
<IonCol size="4">
<h1>£{totalPrice.toFixed(2)}</h1>
</IonCol>
</IonRow>
<IonButton expand="block" color="dark">Checkout &rarr;</IonButton>
</IonFooter>
</IonPage>
);
}

View File

@@ -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 (
<IonContent>
<IonHeader>
<IonToolbar color="none" style={{"--border-style": "none"}}>
<IonTitle className="ion-margin-top">Filter</IonTitle>
</IonToolbar>
</IonHeader>
<IonGrid>
<IonRow>
{filters.map(f => (
<IonCol key={f} size="3">
<IonButton expand="full" color={filterCriteria === f ? "dark" : "light"} onClick={() => filterProducts(f)}>{f}</IonButton>
</IonCol>
))}
</IonRow>
</IonGrid>
</IonContent>
);
}

View File

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

View File

@@ -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 (
<>
<IonContent ref={contentRef}>
<IonButtons id="product-view-buttons">
<IonButton color="light" onClick={dismiss} id="close-button">
<IonIcon icon={closeCircle} size="large" />
</IonButton>
<IonButton color="danger" onClick={() => addToFavourites(product, category)} id="fave-button">
<IonIcon icon={isFavourite ? heart : heartOutline} size="large" />
</IonButton>
</IonButtons>
<IonCard style={{backgroundImage: `url('${product.image}')`}}>
<IonCardHeader>
<IonCardTitle>{product.title}</IonCardTitle>
<IonCardSubtitle>{product.price}</IonCardSubtitle>
</IonCardHeader>
</IonCard>
<div className="ion-padding">
<IonRow className="ion-align-items-center">
<IonCol>
<IonText size="large" className="page-title">
<IonNote>shop</IonNote>
<IonLabel>{category ? category : "Favourite"}</IonLabel>
</IonText>
</IonCol>
<ProductReviews reviews={product.reviews} />
</IonRow>
<h2>Product Description</h2>
<IonText>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.</IonText>
<h2>Product Specifications</h2>
<ProductSpecificationsAccordion contentRef={contentRef} type={category} />
</div>
</IonContent>
<IonFooter collapse="fade">
<IonToolbar>
<IonRow className="ion-justify-content-between ion-align-items-center">
<IonCol size="3">
<IonButton expand="full" color="light">{product.price}</IonButton>
</IonCol>
<IonCol size="8" className="ion-text-right">
<AddToCartButton product={product} />
</IonCol>
</IonRow>
</IonToolbar>
</IonFooter>
</>
);
}

View File

@@ -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 (
<IonCol className="ion-text-right">
<IonIcon color="warning" icon={star} />
&nbsp;&nbsp;
<IonNote>{reviewCount} review{reviewCount > 1 && "s"}</IonNote>
</IonCol>
);
}

View File

@@ -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 (
<IonAccordionGroup ref={accordionGroupRef} onIonChange={log}>
{Object.keys(productSpecs).map((spec, index) => {
const {header, options, wrapText = false, noteColor = false} = productSpecs[spec];
return (
<IonAccordion key={`accordion_${header}_${index}`}>
<IonItem slot="header" className="ion-no-padding">
<IonLabel>{header}</IonLabel>
</IonItem>
<IonList slot="content" className="ion-no-padding">
{options.map((option, index2) => {
const {label, value} = option;
return (
<IonItem key={`accordion_${header}_${option}_${index2}`} className="ion-no-padding">
<IonLabel>
<h3>{label}</h3>
</IonLabel>
<IonLabel className={wrapText && "ion-text-wrap"}>
<IonNote color={noteColor ? (value ? "success" : "danger") : "medium"}>
{noteColor ? (value ? "In stock" : "Out of stock") : value}
</IonNote>
</IonLabel>
</IonItem>
);
})}
</IonList>
</IonAccordion>
);
})}
</IonAccordionGroup>
);
}

View File

@@ -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 (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Ionic Shop</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large" className="page-title">
<IonLabel>ionic</IonLabel>
<IonNote>shop</IonNote>
</IonTitle>
</IonToolbar>
</IonHeader>
<IonRow>
{categories.map(category => (
<IonRouterLink routerLink={`/categories/${category.toLowerCase()}`}>
<div className={styles.categoryContainer}>
<img src={productInfo[category].coverImage} alt="cover" />
<p>{capitalize(category)}</p>
</div>
</IonRouterLink>
// <IonButton key={c} routerLink={`/categories/${c.toLowerCase()}`}>{capitalize(c)}</IonButton>
))}
</IonRow>
</IonContent>
</IonPage>
);
};
export default Categories;

View File

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

View File

@@ -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 (
<IonPage>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonButton className="custom-back" onClick={() => router.goBack()}>
<IonIcon icon={chevronBack} />
<IonLabel>Back</IonLabel>
</IonButton>
</IonButtons>
<IonTitle>{category}</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large" className="page-title">
<IonNote>shop</IonNote>
<IonLabel>{category}</IonLabel>
</IonTitle>
</IonToolbar>
</IonHeader>
<IonRow>
{productTypes.map(product => (
<IonRouterLink key={`${category}_${product}`} routerLink={`/categories/${category}/${product.toLowerCase().replaceAll(" ", "_")}`}>
<div className={styles.categoryContainer}>
<img src={productInfo[category].productTypes[product].coverImage} alt="cover" />
<p>{capitalize(product)}</p>
</div>
</IonRouterLink>
))}
</IonRow>
</IonContent>
</IonPage>
);
};
export default Category;

View File

@@ -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 (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Favourites</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">Favourites</IonTitle>
</IonToolbar>
</IonHeader>
<IonGrid className="animate__animated">
<IonRow>
{favourites.map((product, index) => {
if (product.image !== null && product.image !== "" && !product.image.includes("Placeholder")) {
return (
<IonCol onClick={() => handleProductModal(product)} key={index} size="6" sizeXs="6" sizeSm="3" sizeMd="3" sizeXl="2">
<IonImg src={product.image} style={{marginBottom: "0.25rem"}} />
<IonLabel>
<h3>{product.title}</h3>
<p>{product.price}</p>
</IonLabel>
</IonCol>
);
} else return null;
})}
</IonRow>
{favourites.length === 0 &&
<IonRow className="ion-text-center ion-justify-content-center">
<IonCol size="10">
<IonText color="dark">
<h1>No favourites yet</h1>
</IonText>
<IonText color="medium">
<h3>Add some by clicking the <IonIcon icon={heartOutline} color="danger" /> icon on a product</h3>
</IonText>
</IonCol>
</IonRow>
}
</IonGrid>
</IonContent>
</IonPage>
);
};
export default Favourites;

View File

@@ -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 (
<IonPage>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonButton className="custom-back" onClick={() => router.goBack()}>
<IonIcon icon={chevronBack} />
<IonLabel>Back</IonLabel>
</IonButton>
</IonButtons>
<IonTitle>{capitalize(type)}</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large" className="page-title">
<IonNote>shop</IonNote>
<IonLabel>{category}</IonLabel>
</IonTitle>
</IonToolbar>
</IonHeader>
<IonRow className="ion-align-items-center ion-text-center ion-justify-content-between">
<IonCol size="10">
<IonBreadcrumbs>
<IonBreadcrumb active={false} color="medium">
{capitalize(category)}
</IonBreadcrumb>
<IonBreadcrumb separator={false} color={filterCriteria !== "None" && "medium"} active={filterCriteria === "None" ? true : false}>
{capitalize(type)}
</IonBreadcrumb>
{filterCriteria !== "None" &&
<IonBreadcrumb color="dark" active={true}>
<IonIcon slot="start" icon={filter} />
{filterCriteria}
</IonBreadcrumb>
}
</IonBreadcrumbs>
</IonCol>
<IonCol size="2" className="ion-text-right">
<div onClick={openModal} style={{ display: 'flex', color: "#828282", float:"right", padding: "0.5rem", backgroundColor: "#F4F5F8", marginRight: "0.5rem", width: "fit-content"}}>
<IonIcon icon={filter} />
<IonLabel>&nbsp;Filter</IonLabel>
</div>
</IonCol>
</IonRow>
<IonSearchbar color="light" animated={true} style={{"--border-radius": "none"}} placeholder={`Try '${searchPlaceholder}'`} onIonChange={e => performSearch(e)} />
<IonGrid ref={productsRef} className="animate__animated">
<IonRow>
{filteredProducts.map((product, index) => {
if (product.image !== null && product.image !== "" && !product.image.includes("Placeholder")) {
return (
<IonCol onClick={() => 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"}}>
<IonImg src={product.image} style={{marginBottom: "0.25rem"}} />
<IonLabel>
<h3>{product.title}</h3>
<p>{product.price}</p>
</IonLabel>
</IonCol>
);
} else return null;
})}
</IonRow>
</IonGrid>
</IonContent>
</IonPage>
);
};
export default ProductType;

View File

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

View File

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

View File

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

View File

@@ -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);

View File

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

View File

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

View File

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