update adding demo app,

This commit is contained in:
louiscklaw
2025-06-04 14:46:31 +08:00
parent b78709db9b
commit dff07ddcb0
82 changed files with 3552 additions and 97 deletions

View 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;

View File

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

View 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;

View 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;

View 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;

View 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;

View 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>&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,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,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 &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,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,
},
];

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

View File

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

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