Compare commits
12 Commits
develop/mo
...
develop/mo
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bc35e25616 | ||
![]() |
15f8d2e6aa | ||
![]() |
592a099f7b | ||
![]() |
4c1b30e5c6 | ||
![]() |
c765bb49a4 | ||
![]() |
9aeb58379d | ||
![]() |
6419567005 | ||
![]() |
e2076fe67b | ||
![]() |
766720e075 | ||
![]() |
19af60c410 | ||
![]() |
ed95621b2f | ||
![]() |
2258fd8fb9 |
@@ -0,0 +1,113 @@
|
||||
ion-menu ion-content {
|
||||
--background: var(--ion-item-background, var(--ion-background-color, #fff));
|
||||
}
|
||||
|
||||
ion-menu.md ion-content {
|
||||
--padding-start: 8px;
|
||||
--padding-end: 8px;
|
||||
--padding-top: 20px;
|
||||
--padding-bottom: 20px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-list {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
ion-menu.md ion-note {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-list-header, ion-menu.md ion-note {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-list#inbox-list {
|
||||
border-bottom: 1px solid var(--ion-color-step-150, #d7d8da);
|
||||
}
|
||||
|
||||
ion-menu.md ion-list#inbox-list ion-list-header {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-list#labels-list ion-list-header {
|
||||
font-size: 16px;
|
||||
margin-bottom: 18px;
|
||||
color: #757575;
|
||||
min-height: 26px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-item {
|
||||
--padding-start: 10px;
|
||||
--padding-end: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-item.selected {
|
||||
--background: rgba(var(--ion-color-primary-rgb), 0.14);
|
||||
}
|
||||
|
||||
ion-menu.md ion-item.selected ion-icon {
|
||||
color: var(--ion-color-primary);
|
||||
}
|
||||
|
||||
ion-menu.md ion-item ion-icon {
|
||||
color: #616e7e;
|
||||
}
|
||||
|
||||
ion-menu.md ion-item ion-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-content {
|
||||
--padding-bottom: 20px;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-list {
|
||||
padding: 20px 0 0 0;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-note {
|
||||
line-height: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-item {
|
||||
--padding-start: 16px;
|
||||
--padding-end: 16px;
|
||||
--min-height: 50px;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-item ion-icon {
|
||||
font-size: 24px;
|
||||
color: #73849a;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-item .selected ion-icon {
|
||||
color: var(--ion-color-primary);
|
||||
}
|
||||
|
||||
ion-menu.ios ion-list#labels-list ion-list-header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-list-header,
|
||||
ion-menu.ios ion-note {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-note {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
ion-note {
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
color: var(--ion-color-medium-shade);
|
||||
}
|
||||
|
||||
ion-item.selected {
|
||||
--color: var(--ion-color-primary);
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
import { IonContent, IonIcon, IonItem, IonLabel, IonList, IonListHeader, IonMenu, IonMenuToggle, IonNote } from '@ionic/react';
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { star, starOutline } from 'ionicons/icons';
|
||||
import './Menu.css';
|
||||
|
||||
const Menu = ({ pages }) => {
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<IonMenu contentId="main" type="overlay">
|
||||
<IonContent>
|
||||
<IonList id="inbox-list">
|
||||
<IonListHeader>Overlay Hooks</IonListHeader>
|
||||
<IonNote>Choose one below to see a demo</IonNote>
|
||||
|
||||
{ pages.map((appPage, index) => {
|
||||
|
||||
const isSelected = location.pathname === appPage.url;
|
||||
|
||||
return (
|
||||
<IonMenuToggle key={ index } autoHide={false}>
|
||||
<IonItem className={ isSelected ? 'selected' : '' } routerLink={ appPage.url } routerDirection="none" lines="none" detail={false}>
|
||||
<IonIcon slot="start" icon={ isSelected ? star : starOutline } />
|
||||
<IonLabel>{ appPage.label }</IonLabel>
|
||||
</IonItem>
|
||||
</IonMenuToggle>
|
||||
);
|
||||
})}
|
||||
</IonList>
|
||||
</IonContent>
|
||||
</IonMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default Menu;
|
@@ -0,0 +1,58 @@
|
||||
import { IonButton, IonButtons, IonContent, IonHeader, IonMenuButton, IonPage, IonTitle, IonToolbar, useIonActionSheet } from '@ionic/react';
|
||||
|
||||
const ActionSheet = () => {
|
||||
|
||||
const [ present, dismiss ] = useIonActionSheet();
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Action Sheet</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Action Sheet</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() =>
|
||||
present({
|
||||
buttons: [{ text: 'Ok' }, { text: 'Cancel' }],
|
||||
header: 'Action Sheet'
|
||||
})
|
||||
}
|
||||
>
|
||||
Show ActionSheet
|
||||
</IonButton>
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() =>
|
||||
present([{ text: 'Ok' }, { text: 'Cancel' }], 'Action Sheet')
|
||||
}
|
||||
>
|
||||
Show ActionSheet using params
|
||||
</IonButton>
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() => {
|
||||
present([{ text: 'Ok' }, { text: 'Cancel' }], 'Action Sheet');
|
||||
setTimeout(dismiss, 3000);
|
||||
}}
|
||||
>
|
||||
Show ActionSheet, hide after 3 seconds
|
||||
</IonButton>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionSheet;
|
@@ -0,0 +1,53 @@
|
||||
import { IonButton, IonButtons, IonContent, IonHeader, IonMenuButton, IonPage, IonTitle, IonToolbar, useIonAlert } from '@ionic/react';
|
||||
|
||||
const Alert = () => {
|
||||
|
||||
const [ present ] = useIonAlert();
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Alert</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Alert</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() =>
|
||||
present({
|
||||
cssClass: 'my-css',
|
||||
header: 'Alert',
|
||||
message: 'alert from hook',
|
||||
buttons: [
|
||||
'Cancel',
|
||||
{ text: 'Ok', handler: (d) => console.log('ok pressed') },
|
||||
],
|
||||
onDidDismiss: (e) => console.log('did dismiss'),
|
||||
})
|
||||
}
|
||||
>
|
||||
Show Alert
|
||||
</IonButton>
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() => present('hello with params', [{ text: 'Ok' }])}
|
||||
>
|
||||
Show Alert using params
|
||||
</IonButton>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Alert;
|
@@ -0,0 +1,54 @@
|
||||
import { IonButtons, IonCard, IonCardHeader, IonContent, IonHeader, IonMenuButton, IonPage, IonTitle, IonToolbar, IonCardTitle, IonCardSubtitle, IonCardContent, IonText } from '@ionic/react';
|
||||
|
||||
const All = () => {
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>All</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">All</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonCard>
|
||||
<IonCardHeader>
|
||||
<IonCardSubtitle>Sample usage</IonCardSubtitle>
|
||||
<IonCardTitle>Overlay Hooks</IonCardTitle>
|
||||
</IonCardHeader>
|
||||
|
||||
<IonCardContent>
|
||||
<IonText>
|
||||
<p>
|
||||
In Ionic React 5.6, the team packaged up a new set of hooks for controlling overlay components that they thought we might like. What is an overlay you ask? It’s the term that Ionic give components that display over your current content, such as alerts, modals, toasts, etc.
|
||||
</p>
|
||||
</IonText>
|
||||
<br />
|
||||
<IonText>
|
||||
<p>
|
||||
All of the code is taken from the Ionic Framework docs. You can find the blog post outlining these new overlay hooks <a href="https://ionicframework.com/blog/introducing-the-new-overlay-hooks-for-ionic-react/" target="_blank" rel="noreferrer">here.</a>
|
||||
</p>
|
||||
</IonText>
|
||||
<br />
|
||||
<IonText>
|
||||
<p>
|
||||
Check out the samples by navigating to a respective one in the side menu.
|
||||
</p>
|
||||
</IonText>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default All;
|
@@ -0,0 +1,46 @@
|
||||
import { IonButton, IonButtons, IonContent, IonHeader, IonMenuButton, IonPage, IonTitle, IonToolbar, useIonLoading } from '@ionic/react';
|
||||
|
||||
const Loading = () => {
|
||||
|
||||
const [ present ] = useIonLoading();
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Loading</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Loading</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() =>
|
||||
present({
|
||||
duration: 3000,
|
||||
})
|
||||
}
|
||||
>
|
||||
Show Loading
|
||||
</IonButton>
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() => present('Loading', 2000, 'dots')}
|
||||
>
|
||||
Show Loading using params
|
||||
</IonButton>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
@@ -0,0 +1,68 @@
|
||||
import { IonButton, IonButtons, IonContent, IonHeader, IonMenuButton, IonPage, IonText, IonTitle, IonToolbar, useIonModal } from '@ionic/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
const Modal = () => {
|
||||
|
||||
const Body = ({ count, onDismiss, onIncrement }) => (
|
||||
<div className="ion-text-center">
|
||||
<IonText color="dark" className="ion-text-center">Count: { count }</IonText>
|
||||
<IonButton expand="block" onClick={ () => onIncrement() }>
|
||||
Increment Count
|
||||
</IonButton>
|
||||
<IonButton expand="block" onClick={ () => onDismiss() }>
|
||||
Close
|
||||
</IonButton>
|
||||
</div>
|
||||
);
|
||||
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
const handleIncrement = () => {
|
||||
setCount(count + 1);
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
dismiss();
|
||||
};
|
||||
|
||||
const [present, dismiss] = useIonModal(Body, {
|
||||
count,
|
||||
onDismiss: handleDismiss,
|
||||
onIncrement: handleIncrement,
|
||||
});
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Modal</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Modal</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() => {
|
||||
present({
|
||||
cssClass: 'my-class',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Show Modal
|
||||
</IonButton>
|
||||
<div>Count: {count}</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
@@ -0,0 +1,97 @@
|
||||
import { IonButton, IonButtons, IonContent, IonHeader, IonMenuButton, IonPage, IonTitle, IonToolbar, useIonPicker } from '@ionic/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
const Picker = () => {
|
||||
|
||||
const [ present ] = useIonPicker();
|
||||
const [ value, setValue ] = useState('');
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Picker</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Picker</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() =>
|
||||
present({
|
||||
buttons: [
|
||||
{
|
||||
text: 'Confirm',
|
||||
handler: (selected) => {
|
||||
setValue(selected.animal.value)
|
||||
},
|
||||
},
|
||||
],
|
||||
columns: [
|
||||
{
|
||||
name: 'animal',
|
||||
options: [
|
||||
{ text: 'Dog', value: 'dog' },
|
||||
{ text: 'Cat', value: 'cat' },
|
||||
{ text: 'Bird', value: 'bird' },
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
>
|
||||
Show Picker
|
||||
</IonButton>
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() =>
|
||||
present(
|
||||
[
|
||||
{
|
||||
name: 'animal',
|
||||
options: [
|
||||
{ text: 'Dog', value: 'dog' },
|
||||
{ text: 'Cat', value: 'cat' },
|
||||
{ text: 'Bird', value: 'bird' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'vehicle',
|
||||
options: [
|
||||
{ text: 'Car', value: 'car' },
|
||||
{ text: 'Truck', value: 'truck' },
|
||||
{ text: 'Bike', value: 'bike' },
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
text: 'Confirm',
|
||||
handler: (selected) => {
|
||||
setValue(`${selected.animal.value}, ${selected.vehicle.value}`)
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
}
|
||||
>
|
||||
Show Picker using params
|
||||
</IonButton>
|
||||
{value && (
|
||||
<div>Selected Value: {value}</div>
|
||||
)}
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Picker;
|
@@ -0,0 +1,53 @@
|
||||
import { IonButtons, IonContent, IonHeader, IonItem, IonListHeader, IonMenuButton, IonPage, IonTitle, IonToolbar, IonList, useIonPopover, IonButton } from '@ionic/react';
|
||||
|
||||
const Popover = () => {
|
||||
|
||||
const PopoverList = ({ onHide }) => (
|
||||
<IonList>
|
||||
<IonListHeader>Ionic</IonListHeader>
|
||||
<IonItem button>Learn Ionic</IonItem>
|
||||
<IonItem button>Documentation</IonItem>
|
||||
<IonItem button>Showcase</IonItem>
|
||||
<IonItem button>GitHub Repo</IonItem>
|
||||
<IonItem lines="none" detail={false} button onClick={ onHide }>
|
||||
Close
|
||||
</IonItem>
|
||||
</IonList>
|
||||
);
|
||||
|
||||
const [ present, dismiss ] = useIonPopover(PopoverList, { onHide: () => dismiss() });
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Popover</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Popover</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={(e) =>
|
||||
present({
|
||||
event: e.nativeEvent,
|
||||
})
|
||||
}
|
||||
>
|
||||
Show Popover
|
||||
</IonButton>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Popover;
|
@@ -0,0 +1,52 @@
|
||||
import { IonButton, IonButtons, IonContent, IonHeader, IonMenuButton, IonPage, IonTitle, IonToolbar, useIonToast } from '@ionic/react';
|
||||
|
||||
const Toast = () => {
|
||||
|
||||
const [ present, dismiss ] = useIonToast();
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Toast</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Toast</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() =>
|
||||
present({
|
||||
buttons: [{ text: 'hide', handler: () => dismiss() }],
|
||||
message: 'toast from hook, click hide to dismiss',
|
||||
onDidDismiss: () => console.log('dismissed'),
|
||||
onWillDismiss: () => console.log('will dismiss'),
|
||||
})
|
||||
}
|
||||
>
|
||||
Show Toast
|
||||
</IonButton>
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() => present('hello from hook', 3000)}
|
||||
>
|
||||
Show Toast using params, closes in 3 secs
|
||||
</IonButton>
|
||||
<IonButton expand="block" onClick={dismiss}>
|
||||
Hide Toast
|
||||
</IonButton>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toast;
|
@@ -0,0 +1,77 @@
|
||||
/* 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;
|
||||
}
|
113
03_source/mobile/src/pages/DemoReactQuotes/components/Menu.css
Normal file
113
03_source/mobile/src/pages/DemoReactQuotes/components/Menu.css
Normal file
@@ -0,0 +1,113 @@
|
||||
ion-menu ion-content {
|
||||
--background: var(--ion-item-background, var(--ion-background-color, #fff));
|
||||
}
|
||||
|
||||
ion-menu.md ion-content {
|
||||
--padding-start: 8px;
|
||||
--padding-end: 8px;
|
||||
--padding-top: 20px;
|
||||
--padding-bottom: 20px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-list {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
ion-menu.md ion-note {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-list-header, ion-menu.md ion-note {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-list#inbox-list {
|
||||
border-bottom: 1px solid var(--ion-color-step-150, #d7d8da);
|
||||
}
|
||||
|
||||
ion-menu.md ion-list#inbox-list ion-list-header {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-list#labels-list ion-list-header {
|
||||
font-size: 16px;
|
||||
margin-bottom: 18px;
|
||||
color: #757575;
|
||||
min-height: 26px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-item {
|
||||
--padding-start: 10px;
|
||||
--padding-end: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-item.selected {
|
||||
--background: rgba(var(--ion-color-primary-rgb), 0.14);
|
||||
}
|
||||
|
||||
ion-menu.md ion-item.selected ion-icon {
|
||||
color: var(--ion-color-primary);
|
||||
}
|
||||
|
||||
ion-menu.md ion-item ion-icon {
|
||||
color: #616e7e;
|
||||
}
|
||||
|
||||
ion-menu.md ion-item ion-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-content {
|
||||
--padding-bottom: 20px;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-list {
|
||||
padding: 20px 0 0 0;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-note {
|
||||
line-height: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-item {
|
||||
--padding-start: 16px;
|
||||
--padding-end: 16px;
|
||||
--min-height: 50px;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-item ion-icon {
|
||||
font-size: 24px;
|
||||
color: #73849a;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-item .selected ion-icon {
|
||||
color: var(--ion-color-primary);
|
||||
}
|
||||
|
||||
ion-menu.ios ion-list#labels-list ion-list-header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-list-header,
|
||||
ion-menu.ios ion-note {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-note {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
ion-note {
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
color: var(--ion-color-medium-shade);
|
||||
}
|
||||
|
||||
ion-item.selected {
|
||||
--color: var(--ion-color-primary);
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
IonContent,
|
||||
IonIcon,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonList,
|
||||
IonListHeader,
|
||||
IonMenu,
|
||||
IonMenuToggle,
|
||||
IonNote,
|
||||
} from '@ionic/react';
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { bookmarkOutline, bookmarkSharp, homeOutline, homeSharp } from 'ionicons/icons';
|
||||
import './Menu.css';
|
||||
import { useStoreState } from 'pullstate';
|
||||
import { QuoteStore } from '../store';
|
||||
import { getSavedQuotes } from '../store/Selectors';
|
||||
|
||||
const Menu = () => {
|
||||
|
||||
const location = useLocation();
|
||||
const saved = useStoreState(QuoteStore, getSavedQuotes);
|
||||
|
||||
const appPages = [
|
||||
{
|
||||
title: 'Home',
|
||||
url: '/home',
|
||||
iosIcon: homeOutline,
|
||||
mdIcon: homeSharp
|
||||
},
|
||||
{
|
||||
title: `Bookmarks (${ saved.length })`,
|
||||
url: '/saved',
|
||||
iosIcon: bookmarkOutline,
|
||||
mdIcon: bookmarkSharp
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<IonMenu contentId="main" type="overlay">
|
||||
<IonContent>
|
||||
<IonList id="inbox-list" className="ion-margin-top">
|
||||
<IonListHeader>Ionic Quotes</IonListHeader>
|
||||
<IonNote>hey there!</IonNote>
|
||||
{appPages.map((appPage, index) => {
|
||||
return (
|
||||
<IonMenuToggle key={index} autoHide={false}>
|
||||
<IonItem className={location.pathname === appPage.url ? 'selected' : ''} routerLink={appPage.url} routerDirection="none" lines="none" detail={false}>
|
||||
<IonIcon slot="start" ios={appPage.iosIcon} md={appPage.mdIcon} />
|
||||
<IonLabel>{appPage.title}</IonLabel>
|
||||
</IonItem>
|
||||
</IonMenuToggle>
|
||||
);
|
||||
})}
|
||||
</IonList>
|
||||
</IonContent>
|
||||
</IonMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default Menu;
|
@@ -0,0 +1,21 @@
|
||||
.quoteItem {
|
||||
|
||||
--quote-item-background: rgb(49, 117, 226);
|
||||
|
||||
border: 2px solid rgb(154, 204, 245);
|
||||
border-radius: 10px;
|
||||
--background: var(--quote-item-background);
|
||||
background: var(--quote-item-background);
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.quoteText p {
|
||||
|
||||
color: rgb(25, 51, 93);
|
||||
}
|
||||
|
||||
.quoteText h1:hover {
|
||||
|
||||
color: white;
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
import { IonCol, IonItem, IonLabel } from "@ionic/react";
|
||||
import styles from "./QuoteItem.module.css";
|
||||
|
||||
export const QuoteItem = ({ quote }) => {
|
||||
|
||||
return (
|
||||
|
||||
<IonCol size="6" className="animate__animated animate__fadeIn">
|
||||
<IonItem lines="none" className={ styles.quoteItem } routerLink={ `/quote/${ quote.id }`}>
|
||||
<IonLabel className={ styles.quoteText }>
|
||||
<h2>{ quote.text }</h2>
|
||||
<p>{ quote.author }</p>
|
||||
</IonLabel>
|
||||
</IonItem>
|
||||
</IonCol>
|
||||
);
|
||||
}
|
62
03_source/mobile/src/pages/DemoReactQuotes/pages/Home.tsx
Normal file
62
03_source/mobile/src/pages/DemoReactQuotes/pages/Home.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { IonButtons, IonContent, IonGrid, IonHeader, IonInfiniteScroll, IonInfiniteScrollContent, IonList, IonMenuButton, IonPage, IonRow, IonTitle, IonToolbar } from '@ionic/react';
|
||||
import { useStoreState } from 'pullstate';
|
||||
import { useState } from 'react';
|
||||
import { QuoteItem } from '../components/QuoteItem';
|
||||
import { QuoteStore } from '../store';
|
||||
import { getQuotes } from '../store/Selectors';
|
||||
|
||||
const Home = () => {
|
||||
|
||||
const quotes = useStoreState(QuoteStore, getQuotes);
|
||||
const [ amountLoaded, setAmountLoaded ] = useState(20);
|
||||
|
||||
const fetchMore = async e => {
|
||||
|
||||
setAmountLoaded(amountLoaded => amountLoaded + 20);
|
||||
e.target.complete();
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Home</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Home</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonGrid>
|
||||
<IonList>
|
||||
<IonRow>
|
||||
{ quotes.map((quote, index) => {
|
||||
|
||||
if ((index <= amountLoaded) && quote.author) {
|
||||
return (
|
||||
|
||||
<QuoteItem key={ index } quote={ quote } />
|
||||
);
|
||||
} else return "";
|
||||
})}
|
||||
</IonRow>
|
||||
</IonList>
|
||||
|
||||
<IonInfiniteScroll threshold="200px" onIonInfinite={ fetchMore }>
|
||||
<IonInfiniteScrollContent loadingSpinner="bubbles" loadingText="Getting more quotes...">
|
||||
</IonInfiniteScrollContent>
|
||||
</IonInfiniteScroll>
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
88
03_source/mobile/src/pages/DemoReactQuotes/pages/Quote.tsx
Normal file
88
03_source/mobile/src/pages/DemoReactQuotes/pages/Quote.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { IonBackButton, IonButton, IonButtons, IonCard, IonCardContent, IonCol, IonContent, IonHeader, IonIcon, IonImg, IonPage, IonRow, IonTitle, IonToolbar, useIonToast } from '@ionic/react';
|
||||
import { bookmarkOutline, checkmarkOutline, copyOutline } from 'ionicons/icons';
|
||||
import { useStoreState } from 'pullstate';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { QuoteStore } from '../store';
|
||||
import { addSavedQuote, removeSavedQuote } from '../store/QuoteStore';
|
||||
import { getQuote, getSavedQuotes } from '../store/Selectors';
|
||||
|
||||
import { Clipboard } from '@capacitor/clipboard';
|
||||
|
||||
const Quote = () => {
|
||||
|
||||
const { id } = useParams();
|
||||
const quote = useStoreState(QuoteStore, getQuote(id));
|
||||
const saved = useStoreState(QuoteStore, getSavedQuotes);
|
||||
const [ bookmarked, setBookmarked ] = useState(false);
|
||||
|
||||
const [ present ] = useIonToast();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
setBookmarked(saved.includes(parseInt(id)));
|
||||
}, [ saved, id ]);
|
||||
|
||||
const copyQuote = async () => {
|
||||
|
||||
await Clipboard.write({
|
||||
|
||||
string: quote.text
|
||||
});
|
||||
|
||||
present({
|
||||
|
||||
header: "Success",
|
||||
message: "Quote copied to clipboard!",
|
||||
duration: 2500,
|
||||
color: "primary"
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonBackButton text="Home" />
|
||||
</IonButtons>
|
||||
<IonTitle>Quote</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Quote</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonCard className="animate__animated animate__slideInRight animate__faster">
|
||||
<IonImg src={ quote.image } alt="quote cover" />
|
||||
<IonCardContent>
|
||||
<h1>{ quote.text }</h1>
|
||||
<p>- { quote.author }</p>
|
||||
</IonCardContent>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="6">
|
||||
<IonButton fill={ !bookmarked ? "outline" : "solid" } onClick={ () => bookmarked ? removeSavedQuote(quote.id) : addSavedQuote(quote.id) }>
|
||||
<IonIcon icon={ bookmarked ? checkmarkOutline : bookmarkOutline } />
|
||||
{ bookmarked ? "Bookmarked" : "Save as Bookmark" }
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="4">
|
||||
<IonButton fill="outline" onClick={ copyQuote }>
|
||||
<IonIcon icon={ copyOutline } />
|
||||
Copy Quote
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCard>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Quote;
|
74
03_source/mobile/src/pages/DemoReactQuotes/pages/Saved.tsx
Normal file
74
03_source/mobile/src/pages/DemoReactQuotes/pages/Saved.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { IonButtons, IonCol, IonContent, IonGrid, IonHeader, IonInfiniteScroll, IonInfiniteScrollContent, IonList, IonMenuButton, IonPage, IonRow, IonTitle, IonToolbar } from '@ionic/react';
|
||||
import { useStoreState } from 'pullstate';
|
||||
import { useState } from 'react';
|
||||
import { QuoteItem } from '../components/QuoteItem';
|
||||
import { QuoteStore } from '../store';
|
||||
import { getQuotes, getSavedQuotes } from '../store/Selectors';
|
||||
|
||||
const Saved = () => {
|
||||
|
||||
const quotes = useStoreState(QuoteStore, getQuotes);
|
||||
const saved = useStoreState(QuoteStore, getSavedQuotes);
|
||||
const [ amountLoaded, setAmountLoaded ] = useState(20);
|
||||
|
||||
const fetchMore = async e => {
|
||||
|
||||
setAmountLoaded(amountLoaded => amountLoaded + 20);
|
||||
e.target.complete();
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Bookmarks</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Bookmarks</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonGrid>
|
||||
{ quotes.length > 0 &&
|
||||
|
||||
<IonList>
|
||||
<IonRow>
|
||||
{ quotes.map((quote, index) => {
|
||||
|
||||
if ((index <= amountLoaded) && saved.includes(parseInt(quote.id))) {
|
||||
return (
|
||||
|
||||
<QuoteItem key={ index } quote={ quote } />
|
||||
);
|
||||
} else return "";
|
||||
})}
|
||||
|
||||
<IonInfiniteScroll threshold="200px" onIonInfinite={ fetchMore }>
|
||||
<IonInfiniteScrollContent loadingSpinner="bubbles" loadingText="Getting more quotes...">
|
||||
</IonInfiniteScrollContent>
|
||||
</IonInfiniteScroll>
|
||||
</IonRow>
|
||||
</IonList>
|
||||
}
|
||||
|
||||
{ quotes.length < 1 &&
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<h3>You haven't saved any bookmarks yet.</h3>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
}
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Saved;
|
@@ -0,0 +1,33 @@
|
||||
import { Store } from "pullstate";
|
||||
|
||||
const QuoteStore = new Store({
|
||||
|
||||
quotes: [],
|
||||
saved: []
|
||||
});
|
||||
|
||||
export default QuoteStore;
|
||||
|
||||
export const addSavedQuote = id => {
|
||||
|
||||
QuoteStore.update(s => { s.saved = [ ...s.saved, id ] });
|
||||
}
|
||||
|
||||
export const removeSavedQuote = id => {
|
||||
|
||||
QuoteStore.update(s => { s.saved = s.saved.filter(savedId => parseInt(savedId) !== parseInt(id)) });
|
||||
}
|
||||
|
||||
export const fetchQuotes = async () => {
|
||||
|
||||
const response = await fetch("https://type.fit/api/quotes");
|
||||
const data = await response.json();
|
||||
|
||||
await data.filter((quote, index) => {
|
||||
|
||||
quote.id = (Date.now() + index);
|
||||
quote.image = `https://source.unsplash.com/random/1200x400?sig=${ quote.id }`;
|
||||
});
|
||||
|
||||
QuoteStore.update(s => { s.quotes = data });
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
const getState = state => state;
|
||||
|
||||
// General getters
|
||||
export const getQuotes = createSelector(getState, state => state.quotes);
|
||||
export const getSavedQuotes = createSelector(getState, state => state.saved);
|
||||
|
||||
// Specific getters
|
||||
export const getQuote = id => createSelector(getState, state => state.quotes.filter(q => parseInt(q.id) === parseInt(id))[0]);
|
@@ -0,0 +1 @@
|
||||
export { default as QuoteStore } from "./QuoteStore";
|
@@ -0,0 +1,77 @@
|
||||
/* 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;
|
||||
}
|
@@ -15,7 +15,7 @@ import MembersNearByList from '../MembersNearByList';
|
||||
import OrderList from '../OrderList';
|
||||
import MyProfile from '../MyProfile';
|
||||
import MessageList from '../MessageList';
|
||||
import paths from '../../paths';
|
||||
import PATHS from '../../PATHS';
|
||||
import Favourites from '../Favourites';
|
||||
import TabAppRoute from '../../TabAppRoute';
|
||||
|
||||
@@ -53,23 +53,23 @@ const MainTabs: React.FC<MainTabsProps> = () => {
|
||||
</IonTabButton>
|
||||
*/}
|
||||
|
||||
<IonTabButton tab="events" href={paths.EVENT_LIST}>
|
||||
<IonTabButton tab="events" href={PATHS.EVENT_LIST}>
|
||||
<IonIcon icon={calendar} />
|
||||
<IonLabel>Event</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="nearby" href={paths.NEARBY_LIST}>
|
||||
<IonTabButton tab="nearby" href={PATHS.NEARBY_LIST}>
|
||||
<IonIcon icon={people} />
|
||||
<IonLabel>Nearby</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="orders" href={paths.ORDERS_LIST}>
|
||||
<IonTabButton tab="orders" href={PATHS.ORDERS_LIST}>
|
||||
<IonIcon icon={location} />
|
||||
<IonLabel>Order</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="message" href={paths.MESSAGE_LIST}>
|
||||
<IonTabButton tab="message" href={PATHS.MESSAGE_LIST}>
|
||||
<IonIcon icon={informationCircle} />
|
||||
<IonLabel>Message</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="my_profile" href={paths.PROFILE}>
|
||||
<IonTabButton tab="my_profile" href={PATHS.PROFILE}>
|
||||
<IonIcon icon={informationCircle} />
|
||||
<IonLabel>Profile</IonLabel>
|
||||
</IonTabButton>
|
||||
|
@@ -30,7 +30,7 @@ import { capitalize, productInfo } from '../utils';
|
||||
const ProductType = () => {
|
||||
const router = useIonRouter();
|
||||
const { category, type } = useParams();
|
||||
const productsRef = useRef();
|
||||
const productsRef = useRef(null);
|
||||
|
||||
const [products, setProducts] = useState([]);
|
||||
const [filteredProducts, setFilteredProducts] = useState([]);
|
||||
|
@@ -1,58 +1,50 @@
|
||||
import { CreateAnimation, IonButton, IonIcon } from "@ionic/react";
|
||||
import { cartOutline } from "ionicons/icons";
|
||||
import { useRef, useState } from "react";
|
||||
import { addToCart } from "../store/CartStore";
|
||||
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}) => {
|
||||
export const AddToCartButton = ({ product }) => {
|
||||
const animationRef = useRef(null);
|
||||
const [hidden, setHidden] = useState(true);
|
||||
|
||||
const animationRef = useRef();
|
||||
const [hidden, setHidden] = useState(true);
|
||||
const floatStyle = {
|
||||
display: hidden ? 'none' : '',
|
||||
position: 'absolute',
|
||||
};
|
||||
|
||||
const floatStyle = {
|
||||
const floatGrowAnimation = {
|
||||
property: 'transform',
|
||||
fromValue: 'translateY(0) scale(1)',
|
||||
toValue: 'translateY(-55px) scale(1.5)',
|
||||
};
|
||||
|
||||
display: hidden ? "none" : "",
|
||||
position: "absolute"
|
||||
};
|
||||
const colorAnimation = {
|
||||
property: 'color',
|
||||
fromValue: 'green',
|
||||
toValue: 'green',
|
||||
};
|
||||
|
||||
const floatGrowAnimation = {
|
||||
const mainAnimation = {
|
||||
duration: 1500,
|
||||
iterations: '1',
|
||||
fromTo: [floatGrowAnimation, colorAnimation],
|
||||
easing: 'cubic-bezier(0.25, 0.7, 0.25, 0.7)',
|
||||
};
|
||||
|
||||
property: "transform",
|
||||
fromValue: "translateY(0) scale(1)",
|
||||
toValue: "translateY(-55px) scale(1.5)"
|
||||
};
|
||||
const handleAddToCart = async (product) => {
|
||||
setHidden(false);
|
||||
await animationRef.current.animation.play();
|
||||
setHidden(true);
|
||||
addToCart(product);
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
@@ -18,7 +18,7 @@ import SpeakerDetail from '../SpeakerDetail';
|
||||
import SessionDetail from '../SessionDetail';
|
||||
import MapView from '../MapView';
|
||||
import About from '../About';
|
||||
import paths from '../../paths';
|
||||
import PATHS from '../../PATHS';
|
||||
import TabAppRoute from '../../TabAppRoute';
|
||||
import { CartStore } from './store';
|
||||
import { getCartCount } from './store/Selectors';
|
||||
|
@@ -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,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 →</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>
|
||||
);
|
||||
}
|
@@ -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;
|
@@ -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;
|
||||
}
|
@@ -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;
|
@@ -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;
|
161
03_source/mobile/src/pages/DemoReactShopUi/pages/ProductType.jsx
Normal file
161
03_source/mobile/src/pages/DemoReactShopUi/pages/ProductType.jsx
Normal 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> 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;
|
37
03_source/mobile/src/pages/DemoReactShopUi/pages/index.js
Normal file
37
03_source/mobile/src/pages/DemoReactShopUi/pages/index.js
Normal 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
|
||||
}
|
||||
];
|
@@ -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;
|
||||
}
|
@@ -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);
|
@@ -0,0 +1,2 @@
|
||||
export { default as FavouritesStore } from "./FavouritesStore";
|
||||
export { default as CartStore } from "./CartStore";
|
163
03_source/mobile/src/pages/DemoReactShopUi/theme/variables.css
Normal file
163
03_source/mobile/src/pages/DemoReactShopUi/theme/variables.css
Normal 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;
|
||||
}
|
157
03_source/mobile/src/pages/DemoReactShopUi/utils/index.js
Normal file
157
03_source/mobile/src/pages/DemoReactShopUi/utils/index.js
Normal 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);
|
||||
}
|
@@ -17,7 +17,7 @@ import { chevronBackOutline, refreshOutline } from 'ionicons/icons';
|
||||
|
||||
import './Tab1.css';
|
||||
|
||||
const Tab1 = () => {
|
||||
const Tab1 = (): React.JSX.Element => {
|
||||
const router = useIonRouter();
|
||||
|
||||
function handleBackClick() {
|
@@ -0,0 +1,25 @@
|
||||
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
|
||||
import ExploreContainer from '../components/ExploreContainer';
|
||||
import './Tab2.css';
|
||||
|
||||
const Tab2 = (): React.JSX.Element => {
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Profile</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Profile</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<ExploreContainer name="Profile page" />
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tab2;
|
@@ -0,0 +1,25 @@
|
||||
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
|
||||
import ExploreContainer from '../components/ExploreContainer';
|
||||
import './Tab3.css';
|
||||
|
||||
const Tab3 = (): React.JSX.Element => {
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Settings</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Settings</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<ExploreContainer name="Settings page" />
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tab3;
|
@@ -0,0 +1,24 @@
|
||||
.container {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.container strong {
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.container p {
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
color: #8c8c8c;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.container a {
|
||||
text-decoration: none;
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
import './ExploreContainer.css';
|
||||
|
||||
interface ExploreContainerProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const ExploreContainer = ({ name }: ExploreContainerProps): React.JSX.Element => {
|
||||
return (
|
||||
<div className="container">
|
||||
<strong>{name}</strong>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreContainer;
|
@@ -16,9 +16,24 @@ import { useRef } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Redirect, Route } from 'react-router';
|
||||
|
||||
const SwitchTabBar = () => {
|
||||
interface TabItem {
|
||||
label: string;
|
||||
url: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
backgroundColor: string;
|
||||
component: React.ComponentType;
|
||||
}
|
||||
|
||||
interface AnimationConfig {
|
||||
property: string;
|
||||
fromValue: string;
|
||||
toValue: string;
|
||||
}
|
||||
|
||||
const SwitchTabBar = (): React.JSX.Element => {
|
||||
const [activeTab, setActiveTab] = useState('tab0');
|
||||
const switchRefs = useRef([]);
|
||||
const switchRefs = useRef<(CreateAnimation | null)[]>([]);
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
@@ -61,8 +76,8 @@ const SwitchTabBar = () => {
|
||||
easing: 'ease-in-out',
|
||||
};
|
||||
|
||||
const getTabButtonStyle = (tab) => {
|
||||
const tabStyle = {
|
||||
const getTabButtonStyle = (tab: TabItem): React.CSSProperties => {
|
||||
const tabStyle: React.CSSProperties = {
|
||||
backgroundColor: tab.backgroundColor,
|
||||
color: tab.color,
|
||||
transition: '0.5s all ease-in-out',
|
@@ -0,0 +1,121 @@
|
||||
import { CreateAnimation, IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from "@ionic/react";
|
||||
import { IonReactRouter } from "@ionic/react-router";
|
||||
import { home, person, settings } from 'ionicons/icons';
|
||||
import Tab1 from '../pages/Tab1';
|
||||
import Tab2 from '../pages/Tab2';
|
||||
import Tab3 from '../pages/Tab3';
|
||||
import { useRef } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Redirect, Route } from "react-router";
|
||||
|
||||
const SwitchTabBar = () => {
|
||||
|
||||
const [activeTab, setActiveTab] = useState("tab0");
|
||||
const switchRefs = useRef([]);
|
||||
|
||||
const tabs = [
|
||||
|
||||
{
|
||||
label: "Home",
|
||||
url: "/home",
|
||||
icon: home,
|
||||
color: "#76b140",
|
||||
backgroundColor: "#ddf7c5",
|
||||
component: Tab1
|
||||
},
|
||||
{
|
||||
label: "Profile",
|
||||
url: "/profile",
|
||||
icon: person,
|
||||
color: "#e46062",
|
||||
backgroundColor: "#fcddde",
|
||||
component: Tab2
|
||||
},
|
||||
{
|
||||
label: "Settings",
|
||||
url: "/settings",
|
||||
icon: settings,
|
||||
color: "#3578e5",
|
||||
backgroundColor: "#e7f0ff",
|
||||
component: Tab3
|
||||
}
|
||||
];
|
||||
|
||||
const revealAnimation = {
|
||||
|
||||
property: "transform",
|
||||
fromValue: "translateX(-30px)",
|
||||
toValue: "translateX(0px)"
|
||||
};
|
||||
|
||||
const switchAnimation = {
|
||||
|
||||
duration: 200,
|
||||
direction: "normal",
|
||||
iterations: "1",
|
||||
fromTo: [revealAnimation],
|
||||
easing: "ease-in-out"
|
||||
};
|
||||
|
||||
const getTabButtonStyle = tab => {
|
||||
|
||||
const tabStyle = {
|
||||
|
||||
backgroundColor: tab.backgroundColor,
|
||||
color: tab.color,
|
||||
transition: "0.5s all ease-in-out"
|
||||
};
|
||||
|
||||
return tabStyle;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const tabIndex = activeTab.match(/\d+/)[0];
|
||||
switchRefs.current[tabIndex].animation.play();
|
||||
}, [activeTab]);
|
||||
|
||||
return (
|
||||
|
||||
<IonReactRouter>
|
||||
<IonTabs>
|
||||
<IonRouterOutlet>
|
||||
|
||||
{tabs.map((tab, index) => {
|
||||
|
||||
return (
|
||||
<Route key={index} exact path={tab.url}>
|
||||
<tab.component />
|
||||
</Route>
|
||||
);
|
||||
})}
|
||||
|
||||
<Route exact path="/">
|
||||
<Redirect to="/home" />
|
||||
</Route>
|
||||
</IonRouterOutlet>
|
||||
<IonTabBar slot="bottom" onIonTabsDidChange={e => setActiveTab(e.detail.tab)}>
|
||||
|
||||
{tabs.map((tab, index) => {
|
||||
|
||||
const tabStyle = getTabButtonStyle(tab);
|
||||
const isActive = activeTab === `tab${index}`;
|
||||
|
||||
return (
|
||||
<IonTabButton key={index} style={isActive ? tabStyle : {}} tab={`tab${index}`} href={tab.url}>
|
||||
|
||||
<IonIcon icon={tab.icon} />
|
||||
|
||||
{isActive && <CreateAnimation ref={ref => switchRefs.current[index] = ref} {...switchAnimation}>
|
||||
<IonLabel>{tab.label}</IonLabel>
|
||||
</CreateAnimation>}
|
||||
</IonTabButton>
|
||||
);
|
||||
})}
|
||||
</IonTabBar>
|
||||
</IonTabs>
|
||||
</IonReactRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default SwitchTabBar;
|
@@ -0,0 +1,25 @@
|
||||
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
|
||||
import ExploreContainer from '../components/ExploreContainer';
|
||||
import './Tab1.css';
|
||||
|
||||
const Tab1 = () => {
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Home</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Home</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<ExploreContainer name="Home page" />
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tab1;
|
@@ -0,0 +1,99 @@
|
||||
/* Ionic Variables and Theming. For more info, please see:
|
||||
http://ionicframework.com/docs/theming/ */
|
||||
|
||||
/** Ionic CSS Variables **/
|
||||
:root {
|
||||
/** primary **/
|
||||
--ion-color-primary: #3880ff;
|
||||
--ion-color-primary-rgb: 56, 128, 255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #3171e0;
|
||||
--ion-color-primary-tint: #4c8dff;
|
||||
|
||||
/** secondary **/
|
||||
--ion-color-secondary: #3dc2ff;
|
||||
--ion-color-secondary-rgb: 61, 194, 255;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #36abe0;
|
||||
--ion-color-secondary-tint: #50c8ff;
|
||||
|
||||
/** tertiary **/
|
||||
--ion-color-tertiary: #5260ff;
|
||||
--ion-color-tertiary-rgb: 82, 96, 255;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-tertiary-shade: #4854e0;
|
||||
--ion-color-tertiary-tint: #6370ff;
|
||||
|
||||
/** success **/
|
||||
--ion-color-success: #2dd36f;
|
||||
--ion-color-success-rgb: 45, 211, 111;
|
||||
--ion-color-success-contrast: #ffffff;
|
||||
--ion-color-success-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-success-shade: #28ba62;
|
||||
--ion-color-success-tint: #42d77d;
|
||||
|
||||
/** warning **/
|
||||
--ion-color-warning: #ffc409;
|
||||
--ion-color-warning-rgb: 255, 196, 9;
|
||||
--ion-color-warning-contrast: #000000;
|
||||
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-warning-shade: #e0ac08;
|
||||
--ion-color-warning-tint: #ffca22;
|
||||
|
||||
/** danger **/
|
||||
--ion-color-danger: #eb445a;
|
||||
--ion-color-danger-rgb: 235, 68, 90;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #cf3c4f;
|
||||
--ion-color-danger-tint: #ed576b;
|
||||
|
||||
/** dark **/
|
||||
--ion-color-dark: #222428;
|
||||
--ion-color-dark-rgb: 34, 36, 40;
|
||||
--ion-color-dark-contrast: #ffffff;
|
||||
--ion-color-dark-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-dark-shade: #1e2023;
|
||||
--ion-color-dark-tint: #383a3e;
|
||||
|
||||
/** medium **/
|
||||
--ion-color-medium: #92949c;
|
||||
--ion-color-medium-rgb: 146, 148, 156;
|
||||
--ion-color-medium-contrast: #ffffff;
|
||||
--ion-color-medium-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-medium-shade: #808289;
|
||||
--ion-color-medium-tint: #9d9fa6;
|
||||
|
||||
/** light **/
|
||||
--ion-color-light: #f4f5f8;
|
||||
--ion-color-light-rgb: 244, 245, 248;
|
||||
--ion-color-light-contrast: #000000;
|
||||
--ion-color-light-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-light-shade: #d7d8da;
|
||||
--ion-color-light-tint: #f5f6f9;
|
||||
}
|
||||
|
||||
ion-tab-bar {
|
||||
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
ion-tab-button {
|
||||
|
||||
flex-direction: row;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
ion-tab-button ion-icon {
|
||||
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
ion-tab-button ion-label {
|
||||
|
||||
margin-left: 1rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
@@ -18,7 +18,7 @@ import { SkeletonDashboard } from '../TestComponents/SkeletonDashboard';
|
||||
import { chevronBackOutline, refreshOutline } from 'ionicons/icons';
|
||||
import { CurrentWeather } from '../TestComponents/CurrentWeather';
|
||||
|
||||
function Tab1() {
|
||||
function Tab1(): React.JSX.Element {
|
||||
const router = useIonRouter();
|
||||
|
||||
const [currentWeather, setCurrentWeather] = useState(false);
|
@@ -12,7 +12,7 @@ import {
|
||||
import { useState } from 'react';
|
||||
import { CurrentWeather } from '../TestComponents/CurrentWeather';
|
||||
|
||||
function Tab2() {
|
||||
function Tab2(): React.JSX.Element {
|
||||
const [search, setSearch] = useState('');
|
||||
const [currentWeather, setCurrentWeather] = useState(false);
|
||||
|
@@ -2,8 +2,31 @@ import { IonCardSubtitle, IonCol, IonIcon, IonNote, IonRow } from '@ionic/react'
|
||||
import { pulseOutline, sunnyOutline, thermometerOutline } from 'ionicons/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const WeatherProperty = ({ type, currentWeather }: { type: any; currentWeather: any }) => {
|
||||
const [property, setProperty] = useState(false);
|
||||
interface WeatherPropertyProps {
|
||||
type: 'wind' | 'feelsLike' | 'indexUV' | 'pressure';
|
||||
currentWeather: {
|
||||
current: {
|
||||
wind_mph: number;
|
||||
feelslike_c: number;
|
||||
uv: number;
|
||||
pressure_mb: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface PropertyType {
|
||||
isIcon: boolean;
|
||||
icon: string;
|
||||
alt: string;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const WeatherProperty = ({
|
||||
type,
|
||||
currentWeather,
|
||||
}: WeatherPropertyProps): React.JSX.Element => {
|
||||
const [property, setProperty] = useState<PropertyType | false>(false);
|
||||
|
||||
const properties = {
|
||||
wind: {
|
||||
|
@@ -1,7 +1,24 @@
|
||||
import { IonCard, IonCardContent, IonGrid, IonRow, IonText, IonCardTitle } from '@ionic/react';
|
||||
import { WeatherProperty } from './WeatherProperty';
|
||||
|
||||
export const CurrentWeather = ({ currentWeather }: { currentWeather: any }) => (
|
||||
interface CurrentWeatherProps {
|
||||
currentWeather: {
|
||||
location: {
|
||||
region: string;
|
||||
country: string;
|
||||
localtime: string;
|
||||
};
|
||||
current: {
|
||||
condition: {
|
||||
icon: string;
|
||||
text: string;
|
||||
};
|
||||
temp_c: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const CurrentWeather = ({ currentWeather }: CurrentWeatherProps): React.JSX.Element => (
|
||||
<IonGrid>
|
||||
<IonCard>
|
||||
<IonCardContent className="ion-text-center">
|
||||
|
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
IonGrid,
|
||||
IonRow,
|
||||
IonCol,
|
||||
IonModal,
|
||||
IonButtons,
|
||||
IonButton,
|
||||
IonIcon,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
IonTitle,
|
||||
} from '@ionic/react';
|
||||
import { chevronBack } from 'ionicons/icons';
|
||||
|
||||
interface ModalProps {
|
||||
showModal: boolean;
|
||||
close: (value: boolean) => void;
|
||||
modalOptions: {
|
||||
text?: string;
|
||||
name?: string;
|
||||
icon?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const Modal = (props: ModalProps): React.JSX.Element => (
|
||||
<IonModal isOpen={props.showModal}>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>
|
||||
{props.modalOptions.text ? props.modalOptions.text : props.modalOptions.name}
|
||||
</IonTitle>
|
||||
<IonButtons slot="start">
|
||||
<IonButton onClick={() => props.close(false)}>
|
||||
<IonIcon size="large" icon={chevronBack} style={{ marginLeft: '-0.7rem' }} />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonGrid>
|
||||
<IonRow className="ion-text-center ion-margin-top">
|
||||
<IonCol size="12">
|
||||
<IonIcon style={{ fontSize: '5rem' }} icon={props.modalOptions.icon} />
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
{props.modalOptions.name && (
|
||||
<IonRow className="ion-text-center">
|
||||
<IonCol size="12">
|
||||
<h3>{props.modalOptions.name}</h3>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
)}
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
</IonModal>
|
||||
);
|
@@ -0,0 +1,17 @@
|
||||
import { IonRow, IonCol, IonCardSubtitle, IonCardTitle } from '@ionic/react';
|
||||
|
||||
interface PageHeaderProps {
|
||||
pageName: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export const PageHeader = (props: PageHeaderProps): React.JSX.Element => (
|
||||
<IonRow className="ion-text-center ion-margin-top">
|
||||
<IonCol size="12">
|
||||
<IonCardTitle>Tab Menu with Side Menu</IonCardTitle>
|
||||
<IonCardSubtitle className="ion-margin-top">
|
||||
{props.pageName} page with {props.count} side menu options
|
||||
</IonCardSubtitle>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
);
|
@@ -19,7 +19,11 @@ function DemoReactTabsMenusCustom() {
|
||||
<Tab2 />
|
||||
</Route>
|
||||
|
||||
<Redirect exact path="/demo-react-tabs-menus-custom" to="/demo-react-tabs-menus-custom/tab1" />
|
||||
<Redirect
|
||||
exact
|
||||
path="/demo-react-tabs-menus-custom"
|
||||
to="/demo-react-tabs-menus-custom/tab1"
|
||||
/>
|
||||
</IonRouterOutlet>
|
||||
|
||||
{/* */}
|
||||
|
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { IonHeader, IonContent, IonToolbar, IonTitle, IonButtons, IonMenuButton, IonBackButton, IonIcon, IonSearchbar } from '@ionic/react';
|
||||
import { chevronBack } from 'ionicons/icons';
|
||||
|
||||
const CustomPage = (props) => {
|
||||
|
||||
const mainContent = props.children;
|
||||
const {
|
||||
name,
|
||||
sideMenu = false,
|
||||
sideMenuPosition = "end",
|
||||
backButton = false,
|
||||
backButtonIcon = chevronBack,
|
||||
backButtonText = " ",
|
||||
backButtonPath,
|
||||
actionButton = false,
|
||||
actionButtonPosition,
|
||||
actionButtonIcon,
|
||||
actionButtonIconSize,
|
||||
actionButtonClickEvent,
|
||||
contentClass,
|
||||
searchbar = false,
|
||||
searchbarEvent,
|
||||
showLargeHeader = true
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<IonHeader translucent={ true }>
|
||||
<IonToolbar>
|
||||
<IonTitle>{ name }</IonTitle>
|
||||
|
||||
{ backButton &&
|
||||
<IonButtons slot="start">
|
||||
<IonBackButton icon={ backButtonIcon } text={ backButtonText } defaultHref={ backButtonPath } />
|
||||
</IonButtons>
|
||||
}
|
||||
|
||||
{ (actionButton && actionButtonIcon) &&
|
||||
<IonButtons slot={ actionButtonPosition }>
|
||||
<IonIcon style={{ fontSize: actionButtonIconSize }} icon={ actionButtonIcon } onClick={ actionButtonClickEvent }></IonIcon>
|
||||
</IonButtons>
|
||||
}
|
||||
|
||||
{ sideMenu &&
|
||||
<IonButtons slot={ sideMenuPosition }>
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
}
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent className={ contentClass } fullscreen>
|
||||
|
||||
{ showLargeHeader &&
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle slot="start" size="large">{ name }</IonTitle>
|
||||
{ searchbar && <IonSearchbar style={{ marginTop: "-0.2rem", width: "50%", float: "right" }} onKeyUp={ e => searchbarEvent(e) } onChange={ e => searchbarEvent(e) } /> }
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
}
|
||||
{ mainContent }
|
||||
</IonContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomPage;
|
@@ -0,0 +1,34 @@
|
||||
import { archiveOutline, beerOutline, cogOutline, eyeOutline, golfOutline, logOutOutline, mailOutline, mailUnreadOutline, mapOutline, personOutline, pulseOutline, refreshOutline, restaurantOutline, settingsOutline } from "ionicons/icons";
|
||||
import { buildSideMenuObject } from "./Utils";
|
||||
|
||||
export const tab1SideMenu = [
|
||||
|
||||
buildSideMenuObject(false, "Inbox", "Navigates to Inbox page", mailOutline, "/tabs/tab2"),
|
||||
buildSideMenuObject(false, "Places", "Navigates to Places page", mapOutline, "/tabs/tab3"),
|
||||
buildSideMenuObject(true),
|
||||
buildSideMenuObject(false, "Account Settings", null, settingsOutline, null),
|
||||
buildSideMenuObject(false, "Settings Sub Page", "Opens settings sub page", cogOutline, "/settings"),
|
||||
buildSideMenuObject(false, "Privacy", null, eyeOutline, null),
|
||||
buildSideMenuObject(false, "Logout", null, logOutOutline, null)
|
||||
];
|
||||
|
||||
export const tab2SideMenu = [
|
||||
|
||||
buildSideMenuObject(false, "Profile", "Navigates to Profile page", personOutline, "/tabs/tab1"),
|
||||
buildSideMenuObject(false, "Places", "Navigates to Places page", mapOutline, "/tabs/tab3"),
|
||||
buildSideMenuObject(true),
|
||||
buildSideMenuObject(false, "Unread", null, mailUnreadOutline, null),
|
||||
buildSideMenuObject(false, "Archived", null, archiveOutline, null),
|
||||
buildSideMenuObject(false, "Timestamp style", "Changes the style of the timestamp", refreshOutline, null)
|
||||
];
|
||||
|
||||
export const tab3SideMenu = [
|
||||
|
||||
buildSideMenuObject(false, "Profile", "Navigates to Profile page", personOutline, "/tabs/tab1"),
|
||||
buildSideMenuObject(false, "Inbox", "Navigates to Inbox page", mailOutline, "/tabs/tab2"),
|
||||
buildSideMenuObject(true),
|
||||
buildSideMenuObject(false, "Pubs", null, beerOutline, null),
|
||||
buildSideMenuObject(false, "Restaurants", null, restaurantOutline, null),
|
||||
buildSideMenuObject(false, "Golf Courses", null, golfOutline, null),
|
||||
buildSideMenuObject(false, "Hospitals", null, pulseOutline, null)
|
||||
];
|
@@ -0,0 +1,63 @@
|
||||
import { IonHeader, IonContent, IonToolbar, IonTitle, IonMenuToggle, IonItem, IonIcon, IonMenu, IonLabel, IonList, IonListHeader } from '@ionic/react';
|
||||
import { useSideMenu } from "../main/SideMenuProvider";
|
||||
|
||||
import "../theme/SideMenu.css";
|
||||
|
||||
const SideMenu = (props) => {
|
||||
|
||||
const { type = "overlay" } = props;
|
||||
const mainContent = props.children;
|
||||
const menuOptions = useSideMenu();
|
||||
|
||||
return (
|
||||
<IonMenu contentId={ menuOptions.pageName } side={ menuOptions.side } type={ type }>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Menu</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent forceOverscroll={ false } id="main">
|
||||
|
||||
{ mainContent }
|
||||
|
||||
<IonListHeader>{ menuOptions.pageName }</IonListHeader>
|
||||
|
||||
{ menuOptions !== null &&
|
||||
<IonList lines="none">
|
||||
{ menuOptions && menuOptions.options.map((menuOption, i) => {
|
||||
|
||||
if (menuOption.url === null) {
|
||||
|
||||
return (
|
||||
|
||||
<IonMenuToggle key={ i } autoHide={ true }>
|
||||
<IonItem onClick={ menuOption.clickEvent } lines="none" detail={ false }>
|
||||
<IonIcon slot="start" icon={ menuOption.icon } />
|
||||
<IonLabel>{ menuOption.text }</IonLabel>
|
||||
</IonItem>
|
||||
</IonMenuToggle>
|
||||
);
|
||||
} else {
|
||||
|
||||
if (menuOption.url !== null) {
|
||||
return (
|
||||
|
||||
<IonMenuToggle key={ i } autoHide={ true }>
|
||||
<IonItem detail={ false } routerLink={ menuOption.url } lines="none">
|
||||
<IonIcon slot="start" icon={ menuOption.icon } />
|
||||
<IonLabel>{ menuOption.text }</IonLabel>
|
||||
</IonItem>
|
||||
</IonMenuToggle>
|
||||
);
|
||||
}
|
||||
}
|
||||
})}
|
||||
</IonList>
|
||||
}
|
||||
</IonContent>
|
||||
</IonMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export default SideMenu;
|
@@ -0,0 +1,31 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
|
||||
const SideMenuContext = React.createContext();
|
||||
const SideMenuUpdateContext = React.createContext();
|
||||
|
||||
export function useSideMenu() {
|
||||
return useContext(SideMenuContext);
|
||||
}
|
||||
|
||||
export function useSideMenuUpdate() {
|
||||
|
||||
return useContext(SideMenuUpdateContext);
|
||||
}
|
||||
|
||||
export function SideMenuProvider({ children }) {
|
||||
|
||||
const [ sideMenuOptions, setSideMenuOptions ] = useState({ options: [], side: "", pageName: "" });
|
||||
|
||||
const setSideMenu = (menuOptions) => {
|
||||
|
||||
setSideMenuOptions(menuOptions);
|
||||
}
|
||||
|
||||
return (
|
||||
<SideMenuContext.Provider value={ sideMenuOptions }>
|
||||
<SideMenuUpdateContext.Provider value={ setSideMenu }>
|
||||
{children}
|
||||
</SideMenuUpdateContext.Provider>
|
||||
</SideMenuContext.Provider>
|
||||
);
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
import { IonIcon, IonLabel, IonTabBar, IonTabButton, IonTabs, IonRouterOutlet } from "@ionic/react";
|
||||
import { Redirect, Route } from "react-router-dom";
|
||||
|
||||
const TabMenu = (props) => {
|
||||
|
||||
return (
|
||||
<IonTabs>
|
||||
<IonRouterOutlet>
|
||||
|
||||
{ props.tabs.map((tab, i) => {
|
||||
|
||||
const TabComponent = tab.component;
|
||||
|
||||
if (tab.isTab) {
|
||||
return <Route key={ `tab_route_${ i }` } path={ tab.path } render={ (props) => <TabComponent { ...props } sideMenu={ tab.sideMenu ? true : false } sideMenuOptions={ tab.sideMenuOptions ? tab.sideMenuOptions : false } /> } exact={ true }/>;
|
||||
} else {
|
||||
|
||||
return <Route key={ `child_tab_route_${ i }` } path={ tab.path } render={ (props) => <TabComponent {...props} sideMenu={ tab.sideMenu ? true : false } sideMenuOptions={ tab.sideMenuOptions ? tab.sideMenuOptions : false } /> } exact={ false } />;
|
||||
}
|
||||
})}
|
||||
</IonRouterOutlet>
|
||||
|
||||
<IonTabBar slot={ props.position }>
|
||||
|
||||
{ props.tabs.map((tab, i) => {
|
||||
|
||||
if (tab.isTab) {
|
||||
|
||||
return (
|
||||
<IonTabButton key={ `tab_button_${ i + 1 }` } tab={ `tab_${ i + 1 }` } href={ tab.path }>
|
||||
<IonIcon icon={ tab.icon } />
|
||||
{ tab.label && <IonLabel>{ tab.label }</IonLabel> }
|
||||
</IonTabButton>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</IonTabBar>
|
||||
</IonTabs>
|
||||
);
|
||||
}
|
||||
|
||||
export default TabMenu;
|
@@ -0,0 +1,173 @@
|
||||
import { beerOutline, golfOutline, pulseOutline, restaurantOutline } from "ionicons/icons";
|
||||
|
||||
export const getInboxItems = () => {
|
||||
|
||||
return [
|
||||
|
||||
{
|
||||
id: 1,
|
||||
sender: "Github",
|
||||
subject: "Host your code here",
|
||||
message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
||||
time: "3 mins ago",
|
||||
unread: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
sender: "Ionic",
|
||||
subject: "Amazing cross platform apps on the web",
|
||||
message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
||||
time: "2 hrs ago",
|
||||
unread: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
sender: "Capacitor",
|
||||
subject: "This is why capacitor is awesome",
|
||||
message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
||||
time: "Yesterday",
|
||||
unread: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
sender: "ReactJS",
|
||||
subject: "Get ready for React 2021",
|
||||
message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
||||
time: "Yesterday",
|
||||
unread: true
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
sender: "ContextAPI",
|
||||
subject: "Global state management!",
|
||||
message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
||||
time: "2 days ago",
|
||||
unread: true
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
sender: "Javascript",
|
||||
subject: "The best language",
|
||||
message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
||||
time: "3 days ago",
|
||||
unread: false
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
sender: "Mobile app development",
|
||||
subject: "Bring your solutions to mobile",
|
||||
message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
||||
time: "4 days ago",
|
||||
unread: false
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
export const getInboxItemByID = ID => {
|
||||
|
||||
const inboxItems = getInboxItems();
|
||||
const inboxItem = inboxItems.filter(i => parseInt(i.id) === parseInt(ID))[0];
|
||||
return inboxItem;
|
||||
}
|
||||
|
||||
export const getPlaceItems = () => {
|
||||
|
||||
const places = [
|
||||
{
|
||||
name: "Rusty Tavern",
|
||||
rating: 8,
|
||||
type: "pub",
|
||||
icon: beerOutline
|
||||
},
|
||||
{
|
||||
name: "Meat Mall",
|
||||
rating: 5,
|
||||
type: "restaurant",
|
||||
icon: restaurantOutline
|
||||
},
|
||||
{
|
||||
name: "Lousy Lager",
|
||||
rating: 10,
|
||||
type: "pub",
|
||||
icon: beerOutline
|
||||
},
|
||||
{
|
||||
name: "Hole in one",
|
||||
rating: 4,
|
||||
type: "golf",
|
||||
icon: golfOutline
|
||||
},
|
||||
{
|
||||
name: "Relief center",
|
||||
rating: 9,
|
||||
type: "hospital",
|
||||
icon: pulseOutline
|
||||
},
|
||||
{
|
||||
name: "Yummy yams",
|
||||
rating: 2,
|
||||
type: "restaurant",
|
||||
icon: restaurantOutline
|
||||
},
|
||||
{
|
||||
name: "Under power of others",
|
||||
rating: 7,
|
||||
type: "golf",
|
||||
icon: golfOutline
|
||||
},
|
||||
{
|
||||
name: "Belfast General",
|
||||
rating: 10,
|
||||
type: "hospital",
|
||||
icon: pulseOutline
|
||||
},
|
||||
];
|
||||
|
||||
return places;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Boolean} spacer Renders a space between above and below item
|
||||
* @param {String} text The text or "label" to show
|
||||
* @param {String} description The description to show under the text
|
||||
* @param {*} icon The icon to show - This should be an imported Ion icon
|
||||
* @param {String} url The url to navigate to e.g. "/tabs/tab2"
|
||||
* @param {Function} clickEvent A click event to perform instead of url, leave blank and set in component if it's specific (Should be written like () => function())
|
||||
* @returns A side menu object
|
||||
*/
|
||||
export const buildSideMenuObject = (spacer = false, text = "", description = "", icon = false, url = null, clickEvent = null) => {
|
||||
|
||||
const title = text;
|
||||
|
||||
if (description !== "" && description !== null) {
|
||||
|
||||
text = getInformativeSideMenuItem(text, description);
|
||||
}
|
||||
|
||||
return spacer ? {} : {
|
||||
|
||||
title,
|
||||
text,
|
||||
icon,
|
||||
url,
|
||||
clickEvent
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} text Text of a side menu object
|
||||
* @param {*} description Description of a side menu object
|
||||
* @returns A span and h6 holding the text and description
|
||||
*/
|
||||
const getInformativeSideMenuItem = (text, description) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="menu-title">{ text }</span>
|
||||
<br />
|
||||
<h6 className="sub-menu-title">{ description }</h6>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
// Main Tabs
|
||||
import Tab1 from "../../pages/Tab1"
|
||||
import Tab2 from "../../pages/Tab2";
|
||||
import Tab3 from "../../pages/Tab3";
|
||||
|
||||
// Side Menus
|
||||
import { tab1SideMenu, tab2SideMenu, tab3SideMenu } from "../PageSideMenus";
|
||||
|
||||
// Main tab children
|
||||
import Settings from "../../pages/Settings";
|
||||
|
||||
// Sub pages
|
||||
import InboxItem from "../../pages/InboxItem";
|
||||
|
||||
// Tab icons
|
||||
import { personOutline, mailOutline, mapOutline } from "ionicons/icons";
|
||||
|
||||
// Import custom tab menu
|
||||
import TabMenu from "../TabMenu";
|
||||
import SubRoutes from "./SubRoutes";
|
||||
|
||||
// Array of objects representing tab pages
|
||||
// These will be the main tabs across the app
|
||||
|
||||
// * PARAMS per tab object *
|
||||
// isTab = true will make the tab appear
|
||||
// default = the default tab page to open and be redirected to at "/"
|
||||
// NOTE: there should only be one default tab (default: true)
|
||||
// label = the label to show with the tab
|
||||
// component = the component related to this tab page
|
||||
// icon = icon to show on the tab bar menu
|
||||
// path = the path which the tab is accessible
|
||||
export const tabRoutes = [
|
||||
|
||||
{ label: "Profile", component: Tab1, icon: personOutline, path: "/tabs/tab1", default: true, isTab: true, sideMenu: true, sideMenuOptions: tab1SideMenu },
|
||||
{ label: "Inbox", component: Tab2, icon: mailOutline, path: "/tabs/tab2", default: false, isTab: true, sideMenu: true, sideMenuOptions: tab2SideMenu },
|
||||
{ label: "Places", component: Tab3, icon: mapOutline, path: "/tabs/tab3", default: false, isTab: true, sideMenu: true, sideMenuOptions: tab3SideMenu }
|
||||
];
|
||||
|
||||
// Array of objects representing children pages of tabs
|
||||
|
||||
// * PARAMS per tab object *
|
||||
// isTab = should always be set to false for these
|
||||
// component = the component related to this tab page
|
||||
// path = the path which the tab is accessible
|
||||
|
||||
// These pages should be related to tab pages and be held within the same path
|
||||
// E.g. /tabs/tab1/child
|
||||
const tabChildrenRoutes = [
|
||||
|
||||
{ component: InboxItem, path: "/tabs/tab2/:id", isTab: false },
|
||||
];
|
||||
|
||||
// Array of objects representing sub pages
|
||||
|
||||
// * PARAMS per tab object *
|
||||
// component = the component related to this sub page
|
||||
// path = the path which the sub page is accessible
|
||||
|
||||
// This array should be sub pages which are not directly related to a tab page
|
||||
// E.g. /child
|
||||
const subPageRoutes = [
|
||||
|
||||
{ component: Settings, path: "/settings" },
|
||||
];
|
||||
|
||||
// Let's combine these together as they need to be controlled within the same IonRouterOutlet
|
||||
const tabsAndChildrenRoutes = [ ...tabRoutes, ...tabChildrenRoutes ];
|
||||
|
||||
// Render sub routes
|
||||
export const SubPages = () => ( <SubRoutes routes={ subPageRoutes } /> );
|
||||
|
||||
// Render tab menu
|
||||
export const Tabs = () => ( <TabMenu tabs={ tabsAndChildrenRoutes } position="bottom" /> );
|
@@ -0,0 +1,28 @@
|
||||
import { IonRouterOutlet, IonSplitPane } from "@ionic/react";
|
||||
import { IonReactRouter } from "@ionic/react-router";
|
||||
import { Redirect, Route } from "react-router-dom";
|
||||
import SideMenu from "../SideMenu";
|
||||
import { SubPages, Tabs, tabRoutes } from "./AllRoutes";
|
||||
|
||||
const NavRoutes = () => {
|
||||
|
||||
return (
|
||||
<IonReactRouter>
|
||||
<IonSplitPane contentId="main">
|
||||
|
||||
<SideMenu />
|
||||
|
||||
<IonRouterOutlet id="main">
|
||||
|
||||
<Route path="/tabs" render={ () => <Tabs />} />
|
||||
<SubPages />
|
||||
|
||||
<Route path="/" component={ tabRoutes.filter(t => t.default)[0].component } exact={ true } />
|
||||
<Redirect exact from="/" to={ tabRoutes.filter(t => t.default)[0].path.toString() }/>
|
||||
</IonRouterOutlet>
|
||||
</IonSplitPane>
|
||||
</IonReactRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default NavRoutes;
|
@@ -0,0 +1,17 @@
|
||||
import { Route } from "react-router-dom";
|
||||
|
||||
const SubRoutes = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{ props.routes.map((route, i) => {
|
||||
|
||||
const RouteComponent = route.component;
|
||||
|
||||
return <Route key={ i } path={ route.path } render={ (props) => <RouteComponent { ...props } sideMenu={ route.sideMenu ? true : false } sideMenuOptions={ route.sideMenuOptions ? route.sideMenuOptions : false } /> } exact={ false } />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SubRoutes;
|
@@ -0,0 +1,40 @@
|
||||
#view-inbox-item ion-item {
|
||||
--inner-padding-end: 0;
|
||||
--background: transparent;
|
||||
}
|
||||
|
||||
#view-inbox-item ion-label {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
#view-inbox-item ion-item h2 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#view-inbox-item ion-item .date {
|
||||
float: right;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#view-inbox-item ion-item ion-icon {
|
||||
font-size: 42px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
#view-inbox-item ion-item ion-note {
|
||||
font-size: 15px;
|
||||
margin-right: 12px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#view-inbox-item h1 {
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
#view-inbox-item p {
|
||||
line-height: 22px;
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { personCircle } from 'ionicons/icons';
|
||||
|
||||
import './Tab2.css';
|
||||
import CustomPage from "../main/CustomPage";
|
||||
|
||||
import { IonIcon, IonItem, IonLabel, IonNote, IonPage, useIonViewWillEnter } from '@ionic/react';
|
||||
import { useParams } from "react-router";
|
||||
import { getInboxItemByID } from "../main/Utils";
|
||||
|
||||
import "./InboxItem.css";
|
||||
|
||||
const InboxItem = props => {
|
||||
|
||||
const pageName = "Inbox";
|
||||
const params = useParams();
|
||||
|
||||
const [ inboxItem, setInboxItem ] = useState({});
|
||||
|
||||
useIonViewWillEnter(() => {
|
||||
|
||||
const inboxItemID = params.id;
|
||||
const tempInboxItem = getInboxItemByID(inboxItemID);
|
||||
setInboxItem(tempInboxItem);
|
||||
});
|
||||
|
||||
return (
|
||||
<IonPage id="view-inbox-item">
|
||||
<CustomPage showLargeHeader={ false } name={ pageName } sideMenu={ false } backButton={ true } backButtonText="Inbox">
|
||||
{ inboxItem ? (
|
||||
<>
|
||||
<IonItem>
|
||||
<IonIcon icon={ personCircle } color="primary"></IonIcon>
|
||||
<IonLabel className="ion-text-wrap">
|
||||
<h2>
|
||||
{ inboxItem.sender }
|
||||
<span className="date">
|
||||
<IonNote>{ inboxItem.time }</IonNote>
|
||||
</span>
|
||||
</h2>
|
||||
<h3>
|
||||
To: <IonNote>Me</IonNote>
|
||||
</h3>
|
||||
</IonLabel>
|
||||
</IonItem>
|
||||
|
||||
<div className="ion-padding">
|
||||
<h1>{ inboxItem.subject }</h1>
|
||||
<p>{ inboxItem.message }</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div>Message not found</div>
|
||||
)}
|
||||
</CustomPage>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
|
||||
export default InboxItem;
|
@@ -0,0 +1,27 @@
|
||||
import { IonCol, IonGrid, IonPage, IonRow } from '@ionic/react';
|
||||
import { addOutline } from 'ionicons/icons';
|
||||
|
||||
import './Tab1.css';
|
||||
import CustomPage from "../main/CustomPage";
|
||||
|
||||
const Settings = props => {
|
||||
|
||||
const pageName = "Settings";
|
||||
|
||||
return (
|
||||
<IonPage id={ pageName }>
|
||||
<CustomPage name={ pageName } sideMenu={ false } sideMenuPosition="start" backButton={ true } backButtonText="Profile" actionButton={ true } actionButtonIcon={ addOutline } actionButtonPosition="end" actionButtonIconSize="1.7rem">
|
||||
<IonGrid>
|
||||
|
||||
<IonRow className="ion-text-center">
|
||||
<IonCol size="12">
|
||||
<h3>Sub page</h3>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</CustomPage>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
|
||||
export default Settings;
|
@@ -0,0 +1,5 @@
|
||||
.role {
|
||||
float: right;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
@@ -0,0 +1,94 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { IonAvatar, IonBadge, IonButton, IonCol, IonGrid, IonIcon, IonImg, IonItem, IonLabel, IonNote, IonPage, IonRow, IonText } from '@ionic/react';
|
||||
import { cogOutline, eyeOutline, logOutOutline, mailOutline, mapOutline, settingsOutline } from 'ionicons/icons';
|
||||
|
||||
import './Tab1.css';
|
||||
import CustomPage from "../main/CustomPage";
|
||||
|
||||
import { PageHeader } from "../components/PageHeader";
|
||||
import { Modal } from "../components/Modal";
|
||||
import { useSideMenuUpdate, useSideMenu } from "../main/SideMenuProvider";
|
||||
import { Link } from "react-router-dom";
|
||||
import { tab1SideMenu } from "../main/PageSideMenus";
|
||||
|
||||
const Tab1 = props => {
|
||||
|
||||
const pageName = "Profile";
|
||||
const { sideMenuOptions } = props;
|
||||
const setSideMenu = useSideMenuUpdate();
|
||||
|
||||
const [ showModal, setShowModal ] = useState(false);
|
||||
const [ modalOptions, setModalOptions ] = useState(false);
|
||||
|
||||
const handleModal = async (index) => {
|
||||
|
||||
await setModalOptions(tab1SideMenu[index]);
|
||||
setShowModal(true);
|
||||
}
|
||||
|
||||
// Access other side menu options here
|
||||
const sideMenu = useSideMenu();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (props.location.pathname === "/tabs/tab1") {
|
||||
|
||||
setSideMenu({ options: sideMenuOptions, side: "start", pageName: pageName });
|
||||
}
|
||||
}, [ props.location ]);
|
||||
|
||||
return (
|
||||
<IonPage id={ pageName }>
|
||||
<CustomPage name={ pageName } sideMenu={ true } sideMenuPosition="start">
|
||||
|
||||
<PageHeader count={ sideMenuOptions.length } pageName={ pageName } />
|
||||
|
||||
<IonItem lines="none">
|
||||
<IonAvatar>
|
||||
<IonImg src="/assets/alan.jpg" />
|
||||
</IonAvatar>
|
||||
<IonLabel className="ion-text-wrap ion-padding">
|
||||
<h1>Author</h1>
|
||||
<h2>
|
||||
Alan Montgomery
|
||||
<span className="role">
|
||||
<IonBadge color="primary">Mobile Team Lead</IonBadge>
|
||||
</span>
|
||||
</h2>
|
||||
<p>
|
||||
Hey there, I'm Alan! Hopefully you can take something away from this little sample app. Or even if it's to have a poke around and see how I personally like to do things, that's OK too 👏🏻. Check out each page, side menu and have a look at how things work.
|
||||
</p>
|
||||
</IonLabel>
|
||||
</IonItem>
|
||||
<IonGrid>
|
||||
<IonRow className="ion-text-center">
|
||||
<IonCol size="12">
|
||||
<IonText color="primary">
|
||||
<p>Contact me on twitter if you need anything else :)</p>
|
||||
<a href="https://twitter.com/intent/tweet?screen_name=93alan&ref_src=twsrc%5Etfw" className="twitter-mention-button" data-size="large" data-related="93alan,93alan" data-dnt="true" data-show-count="false">Tweet to @93alan</a>
|
||||
</IonText>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow className="ion-text-center">
|
||||
<IonCol size="12">
|
||||
<IonText>
|
||||
<h4>Check out Mobile DevCast</h4>
|
||||
<p>A podcast dedicated to mobile app development and web native technology like ionic & capacitor!</p>
|
||||
<IonText color="warning">
|
||||
<a style={{ color: "yellow" }} href="https://mobiledevcast.com" target="_blank" rel="noreferrer">https://mobiledevcast.com</a>
|
||||
</IonText>
|
||||
</IonText>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
|
||||
{ (showModal && modalOptions) &&
|
||||
<Modal showModal={ showModal } modalOptions={ modalOptions } close={ () => setShowModal(false) } />
|
||||
}
|
||||
</CustomPage>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
|
||||
export default Tab1;
|
@@ -0,0 +1,85 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { archiveOutline, checkmarkOutline, mailOutline, mailUnreadOutline, mapOutline, personOutline, refreshOutline, settingsSharp } from 'ionicons/icons';
|
||||
import { useSideMenuUpdate, useSideMenu } from "../main/SideMenuProvider";
|
||||
|
||||
import './Tab2.css';
|
||||
import CustomPage from "../main/CustomPage";
|
||||
|
||||
import { PageHeader } from "../components/PageHeader";
|
||||
import { Modal } from "../components/Modal";
|
||||
import { IonBadge, IonChip, IonGrid, IonItem, IonLabel, IonList, IonNote, IonPage } from '@ionic/react';
|
||||
import { getInboxItems } from "../main/Utils";
|
||||
|
||||
const Tab2 = props => {
|
||||
|
||||
const pageName = "Inbox";
|
||||
var { sideMenuOptions } = props;
|
||||
const setSideMenu = useSideMenuUpdate();
|
||||
|
||||
const [ Badge, setBadge ] = useState(true);
|
||||
const [ showModal, setShowModal ] = useState(false);
|
||||
const [ modalOptions, setModalOptions ] = useState(false);
|
||||
|
||||
const inboxItems = getInboxItems();
|
||||
|
||||
const handleModal = async (index) => {
|
||||
|
||||
await setModalOptions(sideMenuOptions[index]);
|
||||
setShowModal(true);
|
||||
}
|
||||
|
||||
// Access other side menu options here
|
||||
const sideMenu = useSideMenu();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (props.location.pathname === "/tabs/tab2") {
|
||||
|
||||
setSideMenu({ options: sideMenuOptions, side: "start", pageName: pageName });
|
||||
|
||||
sideMenuOptions = sideMenuOptions.filter(m => m.title === "Timestamp style")[0].clickEvent = () => setBadge(Badge => !Badge);
|
||||
}
|
||||
}, [ props.location ]);
|
||||
|
||||
return (
|
||||
<IonPage id={ pageName }>
|
||||
<CustomPage name={ pageName } sideMenu={ true } sideMenuPosition="end">
|
||||
<IonGrid>
|
||||
<PageHeader count={ sideMenuOptions.length } pageName={ pageName } />
|
||||
|
||||
<IonList>
|
||||
{ inboxItems.map((item, index) => {
|
||||
|
||||
return (
|
||||
<IonItem routerLink={ `/tabs/tab2/${ item.id }` } key={ `item_${ index }`} detail={ true } lines="full" detailIcon={ item.unread ? mailUnreadOutline : checkmarkOutline }>
|
||||
<IonLabel>
|
||||
<h2>{ item.sender }</h2>
|
||||
<h4>{ item.subject }</h4>
|
||||
<p>{ item.message }</p>
|
||||
</IonLabel>
|
||||
{ Badge &&
|
||||
<IonBadge slot="end" style={{ fontSize: "0.7rem" }}>
|
||||
{ item.time }
|
||||
</IonBadge>
|
||||
}
|
||||
|
||||
{ !Badge &&
|
||||
<IonNote slot="end" style={{ fontSize: "0.9rem" }}>
|
||||
{ item.time }
|
||||
</IonNote>
|
||||
}
|
||||
</IonItem>
|
||||
);
|
||||
})}
|
||||
</IonList>
|
||||
</IonGrid>
|
||||
|
||||
{ (showModal && modalOptions) &&
|
||||
<Modal showModal={ showModal } modalOptions={ modalOptions } close={ () => setShowModal(false) } />
|
||||
}
|
||||
</CustomPage>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
|
||||
export default Tab2;
|
@@ -0,0 +1,97 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSideMenuUpdate, useSideMenu } from "../main/SideMenuProvider";
|
||||
|
||||
import './Tab3.css';
|
||||
import CustomPage from "../main/CustomPage";
|
||||
|
||||
import { PageHeader } from "../components/PageHeader";
|
||||
import { Modal } from "../components/Modal";
|
||||
import { IonPage, IonGrid, IonList, IonItem, IonLabel, IonAvatar, IonIcon, IonBadge } from '@ionic/react';
|
||||
|
||||
import { getPlaceItems } from "../main/Utils";
|
||||
|
||||
const Tab3 = props => {
|
||||
|
||||
const pageName = "Places";
|
||||
const { sideMenuOptions } = props;
|
||||
const setSideMenu = useSideMenuUpdate();
|
||||
|
||||
const initialPlaceItems = getPlaceItems();
|
||||
const [ showModal, setShowModal ] = useState(false);
|
||||
const [ modalOptions, setModalOptions ] = useState(false);
|
||||
const [ placeItems, setPlaceItems ] = useState(initialPlaceItems);
|
||||
|
||||
const handleClick = async (item) => {
|
||||
|
||||
await setModalOptions(item);
|
||||
setShowModal(true);
|
||||
}
|
||||
|
||||
const search = (e) => {
|
||||
|
||||
const searchVal = e.target.value;
|
||||
setPlaceItems(initialPlaceItems);
|
||||
|
||||
if (searchVal !== "") {
|
||||
|
||||
const newItems = initialPlaceItems.filter((item, index) => {
|
||||
if (item.name.toLowerCase().includes(searchVal.toLowerCase())) {
|
||||
|
||||
item.originalIndex = index;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
setPlaceItems(newItems);
|
||||
} else {
|
||||
|
||||
setPlaceItems(initialPlaceItems);
|
||||
}
|
||||
}
|
||||
|
||||
// Access other side menu options here
|
||||
const sideMenu = useSideMenu();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (props.location.pathname === "/tabs/tab3") {
|
||||
|
||||
setSideMenu({ options: sideMenuOptions, side: "start", pageName: pageName });
|
||||
}
|
||||
}, [ props.location ]);
|
||||
|
||||
return (
|
||||
<IonPage id={ pageName }>
|
||||
<CustomPage name={ pageName } sideMenu={ true } sideMenuPosition="start" searchbar={ true } searchbarEvent={ search }>
|
||||
<IonGrid>
|
||||
<PageHeader count={ sideMenuOptions.length } pageName={ pageName } />
|
||||
|
||||
<IonList>
|
||||
{ placeItems.map((item, index) => {
|
||||
|
||||
return (
|
||||
<IonItem onClick={ () => handleClick(item) } key={ `placeItem_${ index }`} detail={ true } lines="full">
|
||||
<IonAvatar>
|
||||
<IonIcon size="large" icon={ item.icon } />
|
||||
</IonAvatar>
|
||||
<IonLabel style={{ padding: "1rem" }}>
|
||||
<h2>{ item.name }</h2>
|
||||
</IonLabel>
|
||||
<IonBadge color="dark" slot="end">
|
||||
{ item.rating } / 10
|
||||
</IonBadge>
|
||||
</IonItem>
|
||||
);
|
||||
})}
|
||||
</IonList>
|
||||
</IonGrid>
|
||||
|
||||
{ (showModal && modalOptions) &&
|
||||
<Modal showModal={ showModal } modalOptions={ modalOptions } close={ () => setShowModal(false) } />
|
||||
}
|
||||
</CustomPage>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
|
||||
export default Tab3;
|
@@ -0,0 +1,24 @@
|
||||
ion-menu ion-content {
|
||||
|
||||
--padding-top: 1.5rem;
|
||||
--padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
ion-menu ion-item {
|
||||
|
||||
--padding-start: 1rem;
|
||||
--min-height: 3.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
ion-menu ion-item ion-icon {
|
||||
|
||||
font-size: 1.6rem;
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
ion-menu ion-item .sub-menu-title {
|
||||
|
||||
color: var(--ion-color-primary) !important;
|
||||
font-size: 0.75rem !important;
|
||||
}
|
@@ -0,0 +1,236 @@
|
||||
/* 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;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
/*
|
||||
* Dark Colors
|
||||
* -------------------------------------------
|
||||
*/
|
||||
|
||||
body {
|
||||
--ion-color-primary: #428cff;
|
||||
--ion-color-primary-rgb: 66,140,255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255,255,255;
|
||||
--ion-color-primary-shade: #3a7be0;
|
||||
--ion-color-primary-tint: #5598ff;
|
||||
|
||||
--ion-color-secondary: #50c8ff;
|
||||
--ion-color-secondary-rgb: 80,200,255;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255,255,255;
|
||||
--ion-color-secondary-shade: #46b0e0;
|
||||
--ion-color-secondary-tint: #62ceff;
|
||||
|
||||
--ion-color-tertiary: #6a64ff;
|
||||
--ion-color-tertiary-rgb: 106,100,255;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255,255,255;
|
||||
--ion-color-tertiary-shade: #5d58e0;
|
||||
--ion-color-tertiary-tint: #7974ff;
|
||||
|
||||
--ion-color-success: #2fdf75;
|
||||
--ion-color-success-rgb: 47,223,117;
|
||||
--ion-color-success-contrast: #000000;
|
||||
--ion-color-success-contrast-rgb: 0,0,0;
|
||||
--ion-color-success-shade: #29c467;
|
||||
--ion-color-success-tint: #44e283;
|
||||
|
||||
--ion-color-warning: #ffd534;
|
||||
--ion-color-warning-rgb: 255,213,52;
|
||||
--ion-color-warning-contrast: #000000;
|
||||
--ion-color-warning-contrast-rgb: 0,0,0;
|
||||
--ion-color-warning-shade: #e0bb2e;
|
||||
--ion-color-warning-tint: #ffd948;
|
||||
|
||||
--ion-color-danger: #ff4961;
|
||||
--ion-color-danger-rgb: 255,73,97;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255,255,255;
|
||||
--ion-color-danger-shade: #e04055;
|
||||
--ion-color-danger-tint: #ff5b71;
|
||||
|
||||
--ion-color-dark: #f4f5f8;
|
||||
--ion-color-dark-rgb: 244,245,248;
|
||||
--ion-color-dark-contrast: #000000;
|
||||
--ion-color-dark-contrast-rgb: 0,0,0;
|
||||
--ion-color-dark-shade: #d7d8da;
|
||||
--ion-color-dark-tint: #f5f6f9;
|
||||
|
||||
--ion-color-medium: #989aa2;
|
||||
--ion-color-medium-rgb: 152,154,162;
|
||||
--ion-color-medium-contrast: #000000;
|
||||
--ion-color-medium-contrast-rgb: 0,0,0;
|
||||
--ion-color-medium-shade: #86888f;
|
||||
--ion-color-medium-tint: #a2a4ab;
|
||||
|
||||
--ion-color-light: #222428;
|
||||
--ion-color-light-rgb: 34,36,40;
|
||||
--ion-color-light-contrast: #ffffff;
|
||||
--ion-color-light-contrast-rgb: 255,255,255;
|
||||
--ion-color-light-shade: #1e2023;
|
||||
--ion-color-light-tint: #383a3e;
|
||||
}
|
||||
|
||||
/*
|
||||
* iOS Dark Theme
|
||||
* -------------------------------------------
|
||||
*/
|
||||
|
||||
.ios body {
|
||||
--ion-background-color: #000000;
|
||||
--ion-background-color-rgb: 0,0,0;
|
||||
|
||||
--ion-text-color: #ffffff;
|
||||
--ion-text-color-rgb: 255,255,255;
|
||||
|
||||
--ion-color-step-50: #0d0d0d;
|
||||
--ion-color-step-100: #1a1a1a;
|
||||
--ion-color-step-150: #262626;
|
||||
--ion-color-step-200: #333333;
|
||||
--ion-color-step-250: #404040;
|
||||
--ion-color-step-300: #4d4d4d;
|
||||
--ion-color-step-350: #595959;
|
||||
--ion-color-step-400: #666666;
|
||||
--ion-color-step-450: #737373;
|
||||
--ion-color-step-500: #808080;
|
||||
--ion-color-step-550: #8c8c8c;
|
||||
--ion-color-step-600: #999999;
|
||||
--ion-color-step-650: #a6a6a6;
|
||||
--ion-color-step-700: #b3b3b3;
|
||||
--ion-color-step-750: #bfbfbf;
|
||||
--ion-color-step-800: #cccccc;
|
||||
--ion-color-step-850: #d9d9d9;
|
||||
--ion-color-step-900: #e6e6e6;
|
||||
--ion-color-step-950: #f2f2f2;
|
||||
|
||||
--ion-item-background: #000000;
|
||||
|
||||
--ion-card-background: #1c1c1d;
|
||||
}
|
||||
|
||||
.ios ion-modal {
|
||||
--ion-background-color: #000000;
|
||||
--ion-toolbar-background: #000000;
|
||||
--ion-toolbar-border-color: var(--ion-color-step-150);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Material Design Dark Theme
|
||||
* -------------------------------------------
|
||||
*/
|
||||
|
||||
.md body {
|
||||
--ion-background-color: #121212;
|
||||
--ion-background-color-rgb: 18,18,18;
|
||||
|
||||
--ion-text-color: #ffffff;
|
||||
--ion-text-color-rgb: 255,255,255;
|
||||
|
||||
--ion-border-color: #222222;
|
||||
|
||||
--ion-color-step-50: #1e1e1e;
|
||||
--ion-color-step-100: #2a2a2a;
|
||||
--ion-color-step-150: #363636;
|
||||
--ion-color-step-200: #414141;
|
||||
--ion-color-step-250: #4d4d4d;
|
||||
--ion-color-step-300: #595959;
|
||||
--ion-color-step-350: #656565;
|
||||
--ion-color-step-400: #717171;
|
||||
--ion-color-step-450: #7d7d7d;
|
||||
--ion-color-step-500: #898989;
|
||||
--ion-color-step-550: #949494;
|
||||
--ion-color-step-600: #a0a0a0;
|
||||
--ion-color-step-650: #acacac;
|
||||
--ion-color-step-700: #b8b8b8;
|
||||
--ion-color-step-750: #c4c4c4;
|
||||
--ion-color-step-800: #d0d0d0;
|
||||
--ion-color-step-850: #dbdbdb;
|
||||
--ion-color-step-900: #e7e7e7;
|
||||
--ion-color-step-950: #f3f3f3;
|
||||
|
||||
--ion-item-background: #1e1e1e;
|
||||
|
||||
--ion-toolbar-background: #1f1f1f;
|
||||
|
||||
--ion-tab-bar-background: #1f1f1f;
|
||||
|
||||
--ion-card-background: #1e1e1e;
|
||||
}
|
||||
}
|
@@ -1,99 +1,102 @@
|
||||
import { IonCard, IonCardHeader, IonCardTitle, IonNote, useIonToast, CreateAnimation, IonIcon } from "@ionic/react";
|
||||
import { useState } from "react";
|
||||
import { useRef } from "react";
|
||||
import { Iconly } from "react-iconly";
|
||||
import { heart, trashBin } from "ionicons/icons";
|
||||
import { addFavourite } from "../store/PlacesStore";
|
||||
import {
|
||||
IonCard,
|
||||
IonCardHeader,
|
||||
IonCardTitle,
|
||||
IonNote,
|
||||
useIonToast,
|
||||
CreateAnimation,
|
||||
IonIcon,
|
||||
} from '@ionic/react';
|
||||
import { useState } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { Iconly } from 'react-iconly';
|
||||
import { heart, trashBin } from 'ionicons/icons';
|
||||
import { addFavourite } from '../store/PlacesStore';
|
||||
|
||||
import styles from "../styles/Home.module.scss";
|
||||
import styles from '../styles/Home.module.scss';
|
||||
|
||||
const PlaceCard = ({ place = false, fromFavourites = false }) => {
|
||||
const animationRef = useRef(null);
|
||||
const cardRef = useRef(null);
|
||||
const [presentToast] = useIonToast();
|
||||
const [hideAnimatedIcon, setHideAnimatedIcon] = useState(true);
|
||||
|
||||
const animationRef = useRef();
|
||||
const cardRef = useRef();
|
||||
const [ presentToast ] = useIonToast();
|
||||
const [ hideAnimatedIcon, setHideAnimatedIcon ] = useState(true);
|
||||
const floatStyle = {
|
||||
display: hideAnimatedIcon ? 'none' : '',
|
||||
position: 'absolute',
|
||||
zIndex: '10',
|
||||
};
|
||||
|
||||
const floatStyle = {
|
||||
const floatGrowAnimation = {
|
||||
property: 'transform',
|
||||
fromValue: 'translateY(0) scale(1)',
|
||||
toValue: 'translateY(-20px) scale(2)',
|
||||
};
|
||||
|
||||
display: hideAnimatedIcon ? "none" : "",
|
||||
position: "absolute",
|
||||
zIndex: "10"
|
||||
};
|
||||
const mainAnimation = {
|
||||
duration: 600,
|
||||
iterations: '1',
|
||||
fromTo: [floatGrowAnimation],
|
||||
easing: 'cubic-bezier(0.25, 0.7, 0.25, 0.7)',
|
||||
};
|
||||
|
||||
const floatGrowAnimation = {
|
||||
const handleAddFavourite = async (e, place) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
property: "transform",
|
||||
fromValue: "translateY(0) scale(1)",
|
||||
toValue: "translateY(-20px) scale(2)"
|
||||
};
|
||||
if (fromFavourites) {
|
||||
// Add a fadeOut animation before removing
|
||||
cardRef.current.classList.add('animate__fadeOut');
|
||||
|
||||
const mainAnimation = {
|
||||
setTimeout(() => {
|
||||
addFavourite(place, fromFavourites);
|
||||
}, 500);
|
||||
} else {
|
||||
addFavourite(place, fromFavourites);
|
||||
}
|
||||
|
||||
duration: 600,
|
||||
iterations: "1",
|
||||
fromTo: [ floatGrowAnimation ],
|
||||
easing: "cubic-bezier(0.25, 0.7, 0.25, 0.7)"
|
||||
};
|
||||
presentToast({
|
||||
header: `Favourite ${fromFavourites ? 'removed' : 'added'}!`,
|
||||
buttons: [
|
||||
{
|
||||
text: '♡',
|
||||
},
|
||||
],
|
||||
message: `${place.name} has been ${fromFavourites ? 'removed from' : 'added to'} your favourites.`,
|
||||
duration: 1500,
|
||||
color: 'success',
|
||||
});
|
||||
|
||||
const handleAddFavourite = async (e, place) => {
|
||||
setHideAnimatedIcon(false);
|
||||
await animationRef.current.animation.play();
|
||||
setHideAnimatedIcon(true);
|
||||
};
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (fromFavourites) {
|
||||
return (
|
||||
<IonCard
|
||||
ref={cardRef}
|
||||
className={`${styles.slide} animate__animated animate__fadeIn animate__faster`}
|
||||
routerLink={`/view-place/${place.id}`}
|
||||
>
|
||||
<div className={styles.imageHeader}>
|
||||
<img src={place ? place.image : '/assets/nonefound.png'} />
|
||||
{place && (
|
||||
<div className="favouriteButton" onClick={(e) => handleAddFavourite(e, place)}>
|
||||
<Iconly set="bold" name={fromFavourites ? 'Delete' : 'Heart'} color="red" />
|
||||
|
||||
// Add a fadeOut animation before removing
|
||||
cardRef.current.classList.add("animate__fadeOut");
|
||||
<CreateAnimation ref={animationRef} {...mainAnimation}>
|
||||
<IonIcon icon={fromFavourites ? trashBin : heart} style={floatStyle} color="danger" />
|
||||
</CreateAnimation>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
setTimeout(() => {
|
||||
addFavourite(place, fromFavourites);
|
||||
}, 500);
|
||||
} else {
|
||||
<IonCardHeader>
|
||||
<IonCardTitle>{place ? place.name : 'Sorry'}</IonCardTitle>
|
||||
<IonNote>{place ? place.destination : 'No results found'}</IonNote>
|
||||
</IonCardHeader>
|
||||
</IonCard>
|
||||
);
|
||||
};
|
||||
|
||||
addFavourite(place, fromFavourites);
|
||||
}
|
||||
|
||||
presentToast({
|
||||
|
||||
header: `Favourite ${ fromFavourites ? "removed" : "added" }!`,
|
||||
buttons: [
|
||||
{
|
||||
text: "♡",
|
||||
}
|
||||
],
|
||||
message: `${ place.name } has been ${ fromFavourites ? "removed from" : "added to" } your favourites.`,
|
||||
duration: 1500,
|
||||
color: "success"
|
||||
});
|
||||
|
||||
setHideAnimatedIcon(false);
|
||||
await animationRef.current.animation.play();
|
||||
setHideAnimatedIcon(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<IonCard ref={ cardRef } className={ `${ styles.slide } animate__animated animate__fadeIn animate__faster` } routerLink={ `/view-place/${ place.id }` }>
|
||||
<div className={ styles.imageHeader }>
|
||||
<img src={ place ? place.image : "/assets/nonefound.png" } />
|
||||
{ place &&
|
||||
<div className="favouriteButton" onClick={ e => handleAddFavourite(e, place) }>
|
||||
|
||||
<Iconly set="bold" name={ fromFavourites ? "Delete" : "Heart" } color="red" />
|
||||
|
||||
<CreateAnimation ref={ animationRef } { ...mainAnimation }>
|
||||
<IonIcon icon={ fromFavourites ? trashBin : heart } style={ floatStyle } color="danger" />
|
||||
</CreateAnimation>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<IonCardHeader>
|
||||
<IonCardTitle>{ place ? place.name : "Sorry" }</IonCardTitle>
|
||||
<IonNote>{ place ? place.destination : "No results found" }</IonNote>
|
||||
</IonCardHeader>
|
||||
</IonCard>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlaceCard;
|
||||
export default PlaceCard;
|
||||
|
@@ -26,7 +26,7 @@ import { BookmarkStore } from '../store';
|
||||
|
||||
const Categories = () => {
|
||||
const router = useIonRouter();
|
||||
const pageRef = useRef();
|
||||
const pageRef = useRef(null);
|
||||
const [recipeCategories, setRecipeCategories] = useState([]);
|
||||
const bookmarks = useStoreState(BookmarkStore, getBookmarks);
|
||||
|
||||
|
@@ -39,7 +39,7 @@ import { useStoreState } from 'pullstate';
|
||||
import { getBookmarks } from '../store/Selectors';
|
||||
|
||||
const Recipe = () => {
|
||||
const pageRef = useRef();
|
||||
const pageRef = useRef(null);
|
||||
const { state } = useLocation();
|
||||
const [recipe, setRecipe] = useState([]);
|
||||
const [fromSearch, setFromSearch] = useState(false);
|
||||
|
@@ -25,7 +25,7 @@ import { performSearch } from '../utils';
|
||||
import { RecipeListItem } from '../components/RecipeListItem';
|
||||
|
||||
const Search = () => {
|
||||
const searchRef = useRef();
|
||||
const searchRef = useRef(null);
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const [showLoader, hideLoader] = useIonLoading();
|
||||
|
||||
|
@@ -0,0 +1,16 @@
|
||||
import { IonItem, IonLabel } from "@ionic/react";
|
||||
import styles from "./Ingredient.module.scss";
|
||||
|
||||
export const Ingredient = ({ ingredient }) => {
|
||||
|
||||
return (
|
||||
|
||||
<IonItem lines="full" className={ styles.ingredientItem }>
|
||||
<img alt="ingredient" src={ ingredient.image } className={ styles.ingredientImage } />
|
||||
<IonLabel className="ion-text-wrap ion-margin-start">
|
||||
<h3>{ ingredient.text }</h3>
|
||||
<p>{ ingredient.weight.toFixed(2) }g</p>
|
||||
</IonLabel>
|
||||
</IonItem>
|
||||
);
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
.ingredientImage {
|
||||
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgb(172, 172, 172);
|
||||
}
|
||||
|
||||
.ingredientItem {
|
||||
|
||||
--padding-top: 1rem;
|
||||
--padding-bottom: 1rem;
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
import { IonButton, IonButtons, IonContent, IonGrid, IonHeader, IonList, IonPage, IonRow, IonTitle, IonToolbar } from "@ionic/react"
|
||||
import { Ingredient } from "./Ingredient";
|
||||
|
||||
const IngredientsModal = ({ dismiss, ingredients}) => {
|
||||
|
||||
return (
|
||||
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>View Ingredients</IonTitle>
|
||||
<IonButtons slot="start">
|
||||
<IonButton color="main" onClick={ dismiss }>Close</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent>
|
||||
<IonList>
|
||||
{ ingredients.map((ingredient, index) => {
|
||||
|
||||
return <Ingredient key={ `ingredient_${ index }` } ingredient={ ingredient } />;
|
||||
})}
|
||||
</IonList>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
|
||||
export default IngredientsModal;
|
@@ -0,0 +1,48 @@
|
||||
import { IonButton, IonButtons, IonCardSubtitle, IonCol, IonContent, IonGrid, IonHeader, IonPage, IonRow, IonTitle, IonToolbar } from "@ionic/react"
|
||||
import NutritionalFact from "./NutritionalFact";
|
||||
|
||||
const NutritionModal = ({ dismiss, recipe }) => {
|
||||
|
||||
return (
|
||||
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>View Nutrition</IonTitle>
|
||||
<IonButtons slot="start">
|
||||
<IonButton color="main" onClick={ dismiss }>Close</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent>
|
||||
|
||||
{ (recipe && recipe.digest) &&
|
||||
|
||||
<IonGrid>
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<IonCardSubtitle className="ion-text-center" color="main">
|
||||
Based on a serving size of { recipe.totalWeight.toFixed(0) }g
|
||||
</IonCardSubtitle>
|
||||
<NutritionalFact type="calories" amount={ recipe.calories.toFixed(0) } />
|
||||
<NutritionalFact type="fat" amount={ recipe.digest[0].total.toFixed(0) } />
|
||||
<NutritionalFact type="trans_fat" amount={ recipe.digest[0].sub[1].total.toFixed(0) } inset={ true } />
|
||||
<NutritionalFact type="saturated_fat" amount={ recipe.digest[0].sub[0].total.toFixed(0) } inset={ true } />
|
||||
<NutritionalFact type="polyunsaturated_fat" amount={ recipe.digest[0].sub[3].total.toFixed(0) } inset={ true } />
|
||||
<NutritionalFact type="monounsaturated_fat" amount={ recipe.digest[0].sub[2].total.toFixed(0) } inset={ true } />
|
||||
<NutritionalFact type="carbs" amount={ recipe.digest[1].total.toFixed(0) } />
|
||||
<NutritionalFact type="sugar" amount={ recipe.digest[1].sub[2].total.toFixed(0) } inset={ true } />
|
||||
<NutritionalFact type="fibre" amount={ recipe.digest[1].sub[1].total.toFixed(0) } inset={ true } />
|
||||
<NutritionalFact type="sugars_added" amount={ recipe.digest[1].sub[3].total.toFixed(0) } inset={ true } />
|
||||
<NutritionalFact type="protein" amount={ recipe.digest[2].total.toFixed(0) } />
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
}
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
|
||||
export default NutritionModal;
|
@@ -0,0 +1,25 @@
|
||||
import { IonCardTitle, IonCol, IonRow } from "@ionic/react";
|
||||
|
||||
const NutritionalFact = ({ type, amount, inset }) => {
|
||||
|
||||
const label = type.replace("_", " ").replace(/(^\w{1})|(\s+\w{1})/g, letter => letter.toUpperCase());
|
||||
|
||||
return (
|
||||
|
||||
<IonRow style={{ borderBottom: "1px solid #242424", padding: "0.5rem" }}>
|
||||
<IonCol size="9">
|
||||
<IonCardTitle style={{ fontSize: "0.9rem", marginLeft: inset ? "1.5rem" : "" }}>
|
||||
{ label }
|
||||
</IonCardTitle>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="3">
|
||||
<IonCardTitle style={{ fontSize: "0.9rem" }}>
|
||||
{ amount }{ type !== "calories" && "g" }
|
||||
</IonCardTitle>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default NutritionalFact;
|
@@ -0,0 +1,18 @@
|
||||
import { IonItem, IonLabel } from "@ionic/react";
|
||||
import { Link } from "react-router-dom";
|
||||
import styles from "./RecipeListItem.module.scss";
|
||||
|
||||
export const RecipeListItem = ({ recipe, fromSearch = false, fromBookmarks = false }) => (
|
||||
|
||||
<Link to={{ pathname: `/recipe/${ recipe.label.replace(" ", "").toLowerCase() }`, state: { recipe, fromSearch, fromBookmarks }}}>
|
||||
<IonItem detail={ true } lines="full" className={ styles.categoryItem }>
|
||||
|
||||
<img src={ recipe.image } alt="cover" className={ styles.categoryImage } />
|
||||
|
||||
<IonLabel className={ styles.categoryDetails }>
|
||||
<h2>{ recipe.label }</h2>
|
||||
<p>{ recipe.dishType && recipe.dishType[0] }</p>
|
||||
</IonLabel>
|
||||
</IonItem>
|
||||
</Link>
|
||||
);
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user