"chore: add barcode scanner and clipboard plugins, update dev script to use yarn, and add new demo pages"
This commit is contained in:
74
03_source/mobile/src/pages/DemoQuoteApp/AppPages/Home.jsx
Normal file
74
03_source/mobile/src/pages/DemoQuoteApp/AppPages/Home.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
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>
|
||||
{/* TODO: the source of the quote is already broken */}
|
||||
<IonList>the source broken</IonList>
|
||||
|
||||
<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/DemoQuoteApp/AppPages/Quote.jsx
Normal file
88
03_source/mobile/src/pages/DemoQuoteApp/AppPages/Quote.jsx
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/DemoQuoteApp/AppPages/Saved.jsx
Normal file
74
03_source/mobile/src/pages/DemoQuoteApp/AppPages/Saved.jsx
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;
|
113
03_source/mobile/src/pages/DemoQuoteApp/components/Menu.css
Normal file
113
03_source/mobile/src/pages/DemoQuoteApp/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);
|
||||
}
|
62
03_source/mobile/src/pages/DemoQuoteApp/components/Menu.jsx
Normal file
62
03_source/mobile/src/pages/DemoQuoteApp/components/Menu.jsx
Normal file
@@ -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,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>
|
||||
);
|
||||
}
|
@@ -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;
|
||||
}
|
27
03_source/mobile/src/pages/DemoQuoteApp/index.tsx
Normal file
27
03_source/mobile/src/pages/DemoQuoteApp/index.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Route, Redirect } from 'react-router';
|
||||
//
|
||||
import Quote from './AppPages/Quote';
|
||||
import Saved from './AppPages/Saved';
|
||||
import Home from './AppPages/Home';
|
||||
//
|
||||
const DemoQuoteApp = () => {
|
||||
return (
|
||||
<>
|
||||
<Route path="/demo-quote-app/home" exact={true}>
|
||||
<Home />
|
||||
</Route>
|
||||
|
||||
<Route path="/demo-quote-app/quote/:id" exact={true}>
|
||||
<Quote />
|
||||
</Route>
|
||||
|
||||
<Route path="/demo-quote-app/saved" exact={true}>
|
||||
<Saved />
|
||||
</Route>
|
||||
|
||||
<Redirect path="/demo-quote-app" exact={true} to="/demo-quote-app/home" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DemoQuoteApp;
|
33
03_source/mobile/src/pages/DemoQuoteApp/store/QuoteStore.js
Normal file
33
03_source/mobile/src/pages/DemoQuoteApp/store/QuoteStore.js
Normal file
@@ -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 });
|
||||
}
|
10
03_source/mobile/src/pages/DemoQuoteApp/store/Selectors.js
Normal file
10
03_source/mobile/src/pages/DemoQuoteApp/store/Selectors.js
Normal file
@@ -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]);
|
1
03_source/mobile/src/pages/DemoQuoteApp/store/index.js
Normal file
1
03_source/mobile/src/pages/DemoQuoteApp/store/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default as QuoteStore } from "./QuoteStore";
|
103
03_source/mobile/src/pages/DemoQuoteApp/style.scss
Normal file
103
03_source/mobile/src/pages/DemoQuoteApp/style.scss
Normal file
@@ -0,0 +1,103 @@
|
||||
#about-page {
|
||||
ion-toolbar {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
--background: transparent;
|
||||
--color: white;
|
||||
}
|
||||
|
||||
ion-toolbar ion-back-button,
|
||||
ion-toolbar ion-button,
|
||||
ion-toolbar ion-menu-button {
|
||||
--color: white;
|
||||
}
|
||||
|
||||
.about-header {
|
||||
position: relative;
|
||||
|
||||
width: 100%;
|
||||
height: 30%;
|
||||
}
|
||||
|
||||
.about-header .about-image {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
transition: opacity 500ms ease-in-out;
|
||||
}
|
||||
|
||||
.about-header .madison {
|
||||
background-image: url('/assets/ScoreBoard/img/about/madison.jpg');
|
||||
}
|
||||
|
||||
.about-header .austin {
|
||||
background-image: url('/assets/ScoreBoard/img/about/austin.jpg');
|
||||
}
|
||||
|
||||
.about-header .chicago {
|
||||
background-image: url('/assets/ScoreBoard/img/about/chicago.jpg');
|
||||
}
|
||||
|
||||
.about-header .seattle {
|
||||
background-image: url('/assets/ScoreBoard/img/about/seattle.jpg');
|
||||
}
|
||||
|
||||
.about-info {
|
||||
position: relative;
|
||||
margin-top: -10px;
|
||||
border-radius: 10px;
|
||||
background: var(--ion-background-color, #fff);
|
||||
z-index: 2; // display rounded border above header image
|
||||
}
|
||||
|
||||
.about-info h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.about-info ion-list {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.about-info p {
|
||||
line-height: 130%;
|
||||
|
||||
color: var(--ion-color-dark);
|
||||
}
|
||||
|
||||
.about-info ion-icon {
|
||||
margin-inline-end: 32px;
|
||||
}
|
||||
|
||||
/*
|
||||
* iOS Only
|
||||
*/
|
||||
|
||||
.ios .about-info {
|
||||
--ion-padding: 19px;
|
||||
}
|
||||
|
||||
.ios .about-info h3 {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
#date-input-popover {
|
||||
--offset-y: -var(--ion-safe-area-bottom);
|
||||
|
||||
--max-width: 90%;
|
||||
--width: 336px;
|
||||
}
|
Reference in New Issue
Block a user