update adding demo app,
This commit is contained in:
89
03_source/mobile/src/pages/DemoReactShop/Categories.jsx
Normal file
89
03_source/mobile/src/pages/DemoReactShop/Categories.jsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonLabel,
|
||||
IonNote,
|
||||
IonPage,
|
||||
IonRouterLink,
|
||||
IonRow,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonRouter,
|
||||
} from '@ionic/react';
|
||||
import {
|
||||
checkmarkOutline,
|
||||
chevronBackOutline,
|
||||
chevronDownCircleOutline,
|
||||
closeOutline,
|
||||
heart,
|
||||
languageOutline,
|
||||
menuOutline,
|
||||
} from 'ionicons/icons';
|
||||
import { capitalize, productInfo } from '../utils';
|
||||
|
||||
const Categories = () => {
|
||||
const categories = Object.keys(productInfo);
|
||||
|
||||
const router = useIonRouter();
|
||||
function handleBackClick() {
|
||||
router.goBack();
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start" onClick={handleBackClick}>
|
||||
<IonButton shape="round" id="open-modal" expand="block">
|
||||
<IonIcon slot="icon-only" icon={chevronBackOutline}></IonIcon>
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
<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, idx) => (
|
||||
<IonRouterLink key={idx} routerLink={`/categories/${category.toLowerCase()}`}>
|
||||
<div style={{ display: 'flex', color: 'white' }}>
|
||||
<img src={productInfo[category].coverImage} alt="cover" />
|
||||
<p
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
position: 'absolute',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
||||
width: '50%',
|
||||
paddingTop: '0.5rem',
|
||||
paddingBottom: '0.5rem',
|
||||
margin: '0 auto',
|
||||
fontSize: '2rem',
|
||||
}}
|
||||
>
|
||||
{capitalize(category)}
|
||||
</p>
|
||||
</div>
|
||||
</IonRouterLink>
|
||||
|
||||
// <IonButton key={c} routerLink={`/categories/${c.toLowerCase()}`}>{capitalize(c)}</IonButton>
|
||||
))}
|
||||
</IonRow>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Categories;
|
@@ -0,0 +1,18 @@
|
||||
.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;
|
||||
}
|
68
03_source/mobile/src/pages/DemoReactShop/Category.jsx
Normal file
68
03_source/mobile/src/pages/DemoReactShop/Category.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
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;
|
102
03_source/mobile/src/pages/DemoReactShop/Favourites.jsx
Normal file
102
03_source/mobile/src/pages/DemoReactShop/Favourites.jsx
Normal file
@@ -0,0 +1,102 @@
|
||||
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;
|
37
03_source/mobile/src/pages/DemoReactShop/Helloworld.tsx
Normal file
37
03_source/mobile/src/pages/DemoReactShop/Helloworld.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
// REQ0041/home_discover_event_tab
|
||||
|
||||
import {
|
||||
IonPage,
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
IonButtons,
|
||||
IonButton,
|
||||
IonIcon,
|
||||
IonTitle,
|
||||
IonContent,
|
||||
} from '@ionic/react';
|
||||
import { menuOutline } from 'ionicons/icons';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import './style.scss';
|
||||
|
||||
const Helloworld: React.FC = () => {
|
||||
return (
|
||||
<IonPage id="speaker-list">
|
||||
<IonHeader translucent={true} className="ion-no-border">
|
||||
<IonToolbar>
|
||||
<IonButtons slot="end">
|
||||
{/* <IonMenuButton /> */}
|
||||
<IonButton shape="round" id="events-open-modal" expand="block">
|
||||
<IonIcon slot="icon-only" icon={menuOutline}></IonIcon>
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
<IonTitle>Discover Events</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen={true}>Helloworld</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Helloworld;
|
81
03_source/mobile/src/pages/DemoReactShop/MainTabs.tsx
Normal file
81
03_source/mobile/src/pages/DemoReactShop/MainTabs.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
// REQ0116/main-tab
|
||||
|
||||
import React from 'react';
|
||||
import { IonTabs, IonRouterOutlet, IonTabBar, IonTabButton, IonIcon, IonLabel } from '@ionic/react';
|
||||
import { Route, Redirect } from 'react-router';
|
||||
import { calendar, location, informationCircle, people } from 'ionicons/icons';
|
||||
import SchedulePage from '../SchedulePage';
|
||||
import SpeakerList from '../SpeakerList';
|
||||
import SpeakerDetail from '../SpeakerDetail';
|
||||
import SessionDetail from '../SessionDetail';
|
||||
import MapView from '../MapView';
|
||||
import About from '../About';
|
||||
import EventList from '../EventList';
|
||||
import MembersNearByList from '../MembersNearByList';
|
||||
import OrderList from '../OrderList';
|
||||
import MyProfile from '../MyProfile';
|
||||
import MessageList from '../MessageList';
|
||||
import paths from '../../paths';
|
||||
import Favourites from '../Favourites';
|
||||
import TabAppRoute from '../../TabAppRoute';
|
||||
|
||||
interface MainTabsProps {}
|
||||
|
||||
const MainTabs: React.FC<MainTabsProps> = () => {
|
||||
return (
|
||||
<IonTabs>
|
||||
<IonRouterOutlet>
|
||||
{/* REQ0117/default-route */}
|
||||
<Redirect exact path="/tabs" to="/tabs/events" />
|
||||
{/*
|
||||
Using the render method prop cuts down the number of renders your components will have due to route changes.
|
||||
Use the component prop when your component depends on the RouterComponentProps passed in automatically.
|
||||
*/}
|
||||
<Route path="/tabs/schedule" render={() => <SchedulePage />} exact={true} />
|
||||
<Route path="/tabs/speakers" render={() => <SpeakerList />} exact={true} />
|
||||
<Route path="/tabs/speakers/:id" component={SpeakerDetail} exact={true} />
|
||||
|
||||
<Route path="/tabs/schedule/:id" component={SessionDetail} />
|
||||
<Route path="/tabs/speakers/sessions/:id" component={SessionDetail} />
|
||||
<Route path="/tabs/map" render={() => <MapView />} exact={true} />
|
||||
|
||||
<Route path="/tabs/about" render={() => <About />} exact={true} />
|
||||
|
||||
{/* */}
|
||||
<TabAppRoute />
|
||||
</IonRouterOutlet>
|
||||
{/* */}
|
||||
<IonTabBar slot="bottom">
|
||||
{/*
|
||||
<IonTabButton tab="speakers" href={'/tabs/speakers'}>
|
||||
<IonIcon icon={calendar} />
|
||||
<IonLabel>Speakers</IonLabel>
|
||||
</IonTabButton>
|
||||
*/}
|
||||
|
||||
<IonTabButton tab="events" href={paths.EVENT_LIST}>
|
||||
<IonIcon icon={calendar} />
|
||||
<IonLabel>Event</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="nearby" href={paths.NEARBY_LIST}>
|
||||
<IonIcon icon={people} />
|
||||
<IonLabel>Nearby</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="orders" href={paths.ORDERS_LIST}>
|
||||
<IonIcon icon={location} />
|
||||
<IonLabel>Order</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="message" href={paths.MESSAGE_LIST}>
|
||||
<IonIcon icon={informationCircle} />
|
||||
<IonLabel>Message</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="my_profile" href={paths.PROFILE}>
|
||||
<IonIcon icon={informationCircle} />
|
||||
<IonLabel>Profile</IonLabel>
|
||||
</IonTabButton>
|
||||
</IonTabBar>
|
||||
</IonTabs>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainTabs;
|
211
03_source/mobile/src/pages/DemoReactShop/ProductType.jsx
Normal file
211
03_source/mobile/src/pages/DemoReactShop/ProductType.jsx
Normal file
@@ -0,0 +1,211 @@
|
||||
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> 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;
|
@@ -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} />
|
||||
Add to Cart
|
||||
|
||||
<CreateAnimation ref={animationRef} {...mainAnimation}>
|
||||
<IonIcon icon={cartOutline} size="large" style={floatStyle} />
|
||||
</CreateAnimation>
|
||||
</IonButton>
|
||||
);
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
@@ -0,0 +1,109 @@
|
||||
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 →
|
||||
</IonButton>
|
||||
</IonFooter>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
@@ -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>
|
||||
);
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -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} />
|
||||
|
||||
<IonNote>{reviewCount} review{reviewCount > 1 && "s"}</IonNote>
|
||||
</IonCol>
|
||||
);
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
129
03_source/mobile/src/pages/DemoReactShop/index.tsx
Normal file
129
03_source/mobile/src/pages/DemoReactShop/index.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
// REQ0116/main-tab
|
||||
|
||||
import React, { useRef, useState } from 'react';
|
||||
import {
|
||||
IonTabs,
|
||||
IonRouterOutlet,
|
||||
IonTabBar,
|
||||
IonTabButton,
|
||||
IonIcon,
|
||||
IonLabel,
|
||||
IonModal,
|
||||
} from '@ionic/react';
|
||||
import { Route, Redirect } from 'react-router';
|
||||
import { calendar, location, informationCircle, people } from 'ionicons/icons';
|
||||
import SchedulePage from '../SchedulePage';
|
||||
import SpeakerList from '../SpeakerList';
|
||||
import SpeakerDetail from '../SpeakerDetail';
|
||||
import SessionDetail from '../SessionDetail';
|
||||
import MapView from '../MapView';
|
||||
import About from '../About';
|
||||
import paths from '../../paths';
|
||||
import TabAppRoute from '../../TabAppRoute';
|
||||
import { CartStore } from './store';
|
||||
import { getCartCount } from './store/Selectors';
|
||||
import { CartModal } from './components/CartModal';
|
||||
|
||||
interface MainTabsProps {}
|
||||
|
||||
const DemoReactShop: React.FC<MainTabsProps> = () => {
|
||||
const cartCount = useStoreState(CartStore, getCartCount);
|
||||
const [selected, setSelected] = useState('tab0');
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef(null);
|
||||
|
||||
const handleClick = (tab) => {
|
||||
tab === 'tabCart' ? setOpen(true) : setSelected(tab);
|
||||
};
|
||||
|
||||
return (
|
||||
<IonTabs>
|
||||
<IonRouterOutlet ref={ref}>
|
||||
{ReactShopPages.map((page, index) => (
|
||||
<Route
|
||||
key={`route_${index}`}
|
||||
exact={true}
|
||||
path={`/demo-react-shop${page.href}`}
|
||||
component={page.component}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/*
|
||||
<Route exact path="/demo-react-shop">
|
||||
<Redirect to={ReactShopPages.filter((p) => p.default)[0].href} />
|
||||
</Route>
|
||||
*/}
|
||||
<Redirect exact path="/demo-react-shop" to="/demo-react-shop/categories" />
|
||||
</IonRouterOutlet>
|
||||
{/* */}
|
||||
|
||||
<IonTabBar slot="bottom">
|
||||
{ReactShopPages.map((page, index) => {
|
||||
const isSelected = selected === `tab${index}`;
|
||||
|
||||
if (page.isTab) {
|
||||
return (
|
||||
<IonTabButton
|
||||
key={`tab${index}`}
|
||||
tab={`tab${index}`}
|
||||
href={`/demo-react-shop${page.href}`}
|
||||
>
|
||||
<IonIcon icon={page.icon} />
|
||||
{isSelected && <div className="tab-dot" />}
|
||||
</IonTabButton>
|
||||
);
|
||||
} else return null;
|
||||
})}
|
||||
|
||||
<IonTabButton tab="tabCart">
|
||||
<IonIcon icon={cartOutline} />
|
||||
<div className="cart-count">{cartCount}</div>
|
||||
</IonTabButton>
|
||||
</IonTabBar>
|
||||
|
||||
{/* <IonModal presentingElement={ref.current} isOpen={open} onDidDismiss={() => setOpen(false)}> */}
|
||||
{/* <CartModal close={() => setOpen(false)} /> */}
|
||||
{/* </IonModal> */}
|
||||
</IonTabs>
|
||||
);
|
||||
};
|
||||
|
||||
export default DemoReactShop;
|
||||
|
||||
import { cartOutline, heartOutline, homeOutline, shirtOutline } from 'ionicons/icons';
|
||||
|
||||
import Categories from './Categories';
|
||||
import Favourites from './Favourites';
|
||||
import ProductType from './ProductType';
|
||||
import Category from './Category';
|
||||
import { useStoreState } from 'pullstate';
|
||||
|
||||
export const ReactShopPages = [
|
||||
{
|
||||
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,
|
||||
},
|
||||
];
|
27
03_source/mobile/src/pages/DemoReactShop/store/CartStore.js
Normal file
27
03_source/mobile/src/pages/DemoReactShop/store/CartStore.js
Normal 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;
|
||||
}
|
@@ -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;
|
||||
}
|
10
03_source/mobile/src/pages/DemoReactShop/store/Selectors.js
Normal file
10
03_source/mobile/src/pages/DemoReactShop/store/Selectors.js
Normal file
@@ -0,0 +1,10 @@
|
||||
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);
|
2
03_source/mobile/src/pages/DemoReactShop/store/index.js
Normal file
2
03_source/mobile/src/pages/DemoReactShop/store/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as FavouritesStore } from './FavouritesStore';
|
||||
export { default as CartStore } from './CartStore';
|
103
03_source/mobile/src/pages/DemoReactShop/style.scss
Normal file
103
03_source/mobile/src/pages/DemoReactShop/style.scss
Normal file
@@ -0,0 +1,103 @@
|
||||
#about-page {
|
||||
ion-toolbar {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
--background: transparent;
|
||||
--color: white;
|
||||
}
|
||||
|
||||
ion-toolbar ion-back-button,
|
||||
ion-toolbar ion-button,
|
||||
ion-toolbar ion-menu-button {
|
||||
--color: white;
|
||||
}
|
||||
|
||||
.about-header {
|
||||
position: relative;
|
||||
|
||||
width: 100%;
|
||||
height: 30%;
|
||||
}
|
||||
|
||||
.about-header .about-image {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
transition: opacity 500ms ease-in-out;
|
||||
}
|
||||
|
||||
.about-header .madison {
|
||||
background-image: url('/assets/img/about/madison.jpg');
|
||||
}
|
||||
|
||||
.about-header .austin {
|
||||
background-image: url('/assets/img/about/austin.jpg');
|
||||
}
|
||||
|
||||
.about-header .chicago {
|
||||
background-image: url('/assets/img/about/chicago.jpg');
|
||||
}
|
||||
|
||||
.about-header .seattle {
|
||||
background-image: url('/assets/img/about/seattle.jpg');
|
||||
}
|
||||
|
||||
.about-info {
|
||||
position: relative;
|
||||
margin-top: -10px;
|
||||
border-radius: 10px;
|
||||
background: var(--ion-background-color, #fff);
|
||||
z-index: 2; // display rounded border above header image
|
||||
}
|
||||
|
||||
.about-info h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.about-info ion-list {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.about-info p {
|
||||
line-height: 130%;
|
||||
|
||||
color: var(--ion-color-dark);
|
||||
}
|
||||
|
||||
.about-info ion-icon {
|
||||
margin-inline-end: 32px;
|
||||
}
|
||||
|
||||
/*
|
||||
* iOS Only
|
||||
*/
|
||||
|
||||
.ios .about-info {
|
||||
--ion-padding: 19px;
|
||||
}
|
||||
|
||||
.ios .about-info h3 {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
#date-input-popover {
|
||||
--offset-y: -var(--ion-safe-area-bottom);
|
||||
|
||||
--max-width: 90%;
|
||||
--width: 336px;
|
||||
}
|
Reference in New Issue
Block a user