Compare commits
4 Commits
develop/mo
...
develop/mo
Author | SHA1 | Date | |
---|---|---|---|
![]() |
19af60c410 | ||
![]() |
ed95621b2f | ||
![]() |
2258fd8fb9 | ||
![]() |
0f674badd9 |
@@ -0,0 +1,59 @@
|
||||
import { IonSlide, IonButton, IonGrid, IonRow, IonCol } from '@ionic/react';
|
||||
import '../pages/Home.css';
|
||||
|
||||
interface OnboardingSlideProps {
|
||||
image: string;
|
||||
mainSlide?: boolean;
|
||||
finalSlide?: boolean;
|
||||
title: string;
|
||||
text: string;
|
||||
lastSlide?: boolean;
|
||||
sliderRef: React.RefObject<any>;
|
||||
}
|
||||
|
||||
const OnboardingSlide = ({
|
||||
image,
|
||||
mainSlide = false,
|
||||
finalSlide = false,
|
||||
title,
|
||||
text,
|
||||
lastSlide,
|
||||
sliderRef,
|
||||
}: OnboardingSlideProps): React.JSX.Element => {
|
||||
return (
|
||||
<IonSlide>
|
||||
<IonGrid className="ion-justify-content-center ion-align-items-center ion-align-self-center">
|
||||
<IonRow className="slide-content-container">
|
||||
<IonCol size="10" className="slide-content">
|
||||
<img src={image} className={mainSlide && 'slide-main-image'} />
|
||||
<h1>{title}</h1>
|
||||
<p>{text}</p>
|
||||
|
||||
{mainSlide && (
|
||||
<IonButton
|
||||
expand="block"
|
||||
fill="outline"
|
||||
onClick={() => sliderRef.current.slideNext()}
|
||||
>
|
||||
Get started →
|
||||
</IonButton>
|
||||
)}
|
||||
|
||||
{finalSlide && (
|
||||
<>
|
||||
<IonButton expand="block" fill="solid">
|
||||
Register
|
||||
</IonButton>
|
||||
<IonButton expand="block" fill="outline">
|
||||
Login
|
||||
</IonButton>
|
||||
</>
|
||||
)}
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonSlide>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingSlide;
|
@@ -0,0 +1,52 @@
|
||||
ion-slides {
|
||||
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.slide-grid {
|
||||
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.slide-main-image {
|
||||
|
||||
height: 5rem !important;
|
||||
}
|
||||
|
||||
.slide-buttons {
|
||||
|
||||
position: absolute;
|
||||
bottom: 2rem;
|
||||
z-index: 10;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.slide-content-container {
|
||||
|
||||
margin-top: -4rem;
|
||||
}
|
||||
|
||||
.slide-content {
|
||||
|
||||
margin: 0 auto;
|
||||
/* margin-top: 5rem; */
|
||||
color: var(--ion-color-primary);
|
||||
/* background-color: var(--ion-color-primary); */
|
||||
/* color: white; */
|
||||
border: 2px solid rgb(228, 228, 228);
|
||||
border-radius: 15px;
|
||||
padding: 3rem;
|
||||
/* padding-left: 3rem; */
|
||||
/* padding-right: 3rem; */
|
||||
}
|
||||
|
||||
.slide-content p {
|
||||
|
||||
color: rgb(161, 161, 161);
|
||||
}
|
@@ -0,0 +1,89 @@
|
||||
import { IonButton, IonContent, IonIcon, IonPage, IonRow, IonSlides } from '@ionic/react';
|
||||
import { arrowBack, arrowForward } from 'ionicons/icons';
|
||||
import { useRef, useState } from 'react';
|
||||
import OnboardingSlide from '../components/OnboardingSlide';
|
||||
import './Home.css';
|
||||
|
||||
interface SlideContent {
|
||||
image: string;
|
||||
mainSlide?: boolean;
|
||||
finalSlide?: boolean;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const Home = (): React.JSX.Element => {
|
||||
const sliderRef = useRef<HTMLIonSlidesElement>(null);
|
||||
const [lastSlide, setLastSlide] = useState(false);
|
||||
const [firstSlide, setFirstSlide] = useState(true);
|
||||
|
||||
const slideContent: SlideContent[] = [
|
||||
{
|
||||
image: '/assets/applogo1.png',
|
||||
mainSlide: true,
|
||||
title: 'Ionic Onboarding UI',
|
||||
text: 'Share moments with your followers and experience memorable captures',
|
||||
},
|
||||
{
|
||||
image: '/assets/1sub.png',
|
||||
title: 'Capture',
|
||||
text: 'Capture that perfect moment in your life',
|
||||
},
|
||||
{
|
||||
image: '/assets/2sub.png',
|
||||
title: 'Organize',
|
||||
text: 'Organize photos exactly how you want them',
|
||||
},
|
||||
{
|
||||
image: '/assets/3sub.png',
|
||||
title: 'Share',
|
||||
finalSlide: true,
|
||||
text: 'Are you ready to share your special moments online with the world?',
|
||||
},
|
||||
];
|
||||
|
||||
const checkSlides = async () => {
|
||||
if (!sliderRef.current) return;
|
||||
|
||||
const isLastSlide = await sliderRef.current.isEnd();
|
||||
const isFirstSlide = await sliderRef.current.isBeginning();
|
||||
setLastSlide(isLastSlide);
|
||||
setFirstSlide(isFirstSlide);
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonContent fullscreen>
|
||||
<IonSlides
|
||||
onIonSlideWillChange={checkSlides}
|
||||
pager={true}
|
||||
ref={sliderRef}
|
||||
id="slider"
|
||||
options={{ slidesPerView: 'auto', zoom: true, grabCursor: true }}
|
||||
>
|
||||
{slideContent.map((slide, index) => {
|
||||
return (
|
||||
<OnboardingSlide key={index} {...slide} lastSlide={lastSlide} sliderRef={sliderRef} />
|
||||
);
|
||||
})}
|
||||
</IonSlides>
|
||||
|
||||
<IonRow className="slide-buttons">
|
||||
{!firstSlide && (
|
||||
<IonButton fill="clear" onClick={() => sliderRef.current?.slidePrev()}>
|
||||
<IonIcon icon={arrowBack} />
|
||||
</IonButton>
|
||||
)}
|
||||
|
||||
{!lastSlide && (
|
||||
<IonButton fill="clear" onClick={() => sliderRef.current?.slideNext()}>
|
||||
<IonIcon icon={arrowForward} />
|
||||
</IonButton>
|
||||
)}
|
||||
</IonRow>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
@@ -0,0 +1,82 @@
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.swiper-pagination {
|
||||
|
||||
margin-bottom: 1rem;
|
||||
}
|
@@ -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';
|
||||
|
Reference in New Issue
Block a user