update demo-react-quotes,
This commit is contained in:
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;
|
||||
}
|
Reference in New Issue
Block a user