diff --git a/03_source/mobile/src/pages/DemoReactQuotes/AppPages/Tab1.jsx b/03_source/mobile/src/pages/DemoReactQuotes/AppPages/Tab1.tsx similarity index 100% rename from 03_source/mobile/src/pages/DemoReactQuotes/AppPages/Tab1.jsx rename to 03_source/mobile/src/pages/DemoReactQuotes/AppPages/Tab1.tsx diff --git a/03_source/mobile/src/pages/DemoReactQuotes/AppPages/Tab2.jsx b/03_source/mobile/src/pages/DemoReactQuotes/AppPages/Tab2.tsx similarity index 100% rename from 03_source/mobile/src/pages/DemoReactQuotes/AppPages/Tab2.jsx rename to 03_source/mobile/src/pages/DemoReactQuotes/AppPages/Tab2.tsx diff --git a/03_source/mobile/src/pages/DemoReactQuotes/components/Menu.css b/03_source/mobile/src/pages/DemoReactQuotes/components/Menu.css new file mode 100644 index 0000000..0ca47a2 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactQuotes/components/Menu.css @@ -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); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactQuotes/components/Menu.tsx b/03_source/mobile/src/pages/DemoReactQuotes/components/Menu.tsx new file mode 100644 index 0000000..c42c9e3 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactQuotes/components/Menu.tsx @@ -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 ( + + + + Ionic Quotes + hey there! + {appPages.map((appPage, index) => { + return ( + + + + {appPage.title} + + + ); + })} + + + + ); +}; + +export default Menu; diff --git a/03_source/mobile/src/pages/DemoReactQuotes/components/QuoteItem.module.css b/03_source/mobile/src/pages/DemoReactQuotes/components/QuoteItem.module.css new file mode 100644 index 0000000..8baadf6 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactQuotes/components/QuoteItem.module.css @@ -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; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactQuotes/components/QuoteItem.tsx b/03_source/mobile/src/pages/DemoReactQuotes/components/QuoteItem.tsx new file mode 100644 index 0000000..85c516f --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactQuotes/components/QuoteItem.tsx @@ -0,0 +1,17 @@ +import { IonCol, IonItem, IonLabel } from "@ionic/react"; +import styles from "./QuoteItem.module.css"; + +export const QuoteItem = ({ quote }) => { + + return ( + + + + +

{ quote.text }

+

{ quote.author }

+
+
+
+ ); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactQuotes/pages/Home.tsx b/03_source/mobile/src/pages/DemoReactQuotes/pages/Home.tsx new file mode 100644 index 0000000..49d05df --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactQuotes/pages/Home.tsx @@ -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 ( + + + + + + + Home + + + + + + + Home + + + + + + + { quotes.map((quote, index) => { + + if ((index <= amountLoaded) && quote.author) { + return ( + + + ); + } else return ""; + })} + + + + + + + + + + + ); +}; + +export default Home; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactQuotes/pages/Quote.tsx b/03_source/mobile/src/pages/DemoReactQuotes/pages/Quote.tsx new file mode 100644 index 0000000..4efb394 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactQuotes/pages/Quote.tsx @@ -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 ( + + + + + + + Quote + + + + + + + Quote + + + + + + +

{ quote.text }

+

- { quote.author }

+
+ + + + bookmarked ? removeSavedQuote(quote.id) : addSavedQuote(quote.id) }> + +  { bookmarked ? "Bookmarked" : "Save as Bookmark" } + + + + + + +  Copy Quote + + + +
+
+
+ ); +}; + +export default Quote; diff --git a/03_source/mobile/src/pages/DemoReactQuotes/pages/Saved.tsx b/03_source/mobile/src/pages/DemoReactQuotes/pages/Saved.tsx new file mode 100644 index 0000000..171c2a8 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactQuotes/pages/Saved.tsx @@ -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 ( + + + + + + + Bookmarks + + + + + + + Bookmarks + + + + + { quotes.length > 0 && + + + + { quotes.map((quote, index) => { + + if ((index <= amountLoaded) && saved.includes(parseInt(quote.id))) { + return ( + + + ); + } else return ""; + })} + + + + + + + + } + + { quotes.length < 1 && + + +

You haven't saved any bookmarks yet.

+
+
+ } +
+
+
+ ); +}; + +export default Saved; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactQuotes/store/QuoteStore.tsx b/03_source/mobile/src/pages/DemoReactQuotes/store/QuoteStore.tsx new file mode 100644 index 0000000..6740d86 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactQuotes/store/QuoteStore.tsx @@ -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 }); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactQuotes/store/Selectors.tsx b/03_source/mobile/src/pages/DemoReactQuotes/store/Selectors.tsx new file mode 100644 index 0000000..a86119c --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactQuotes/store/Selectors.tsx @@ -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]); \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactQuotes/store/index.tsx b/03_source/mobile/src/pages/DemoReactQuotes/store/index.tsx new file mode 100644 index 0000000..573f0a4 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactQuotes/store/index.tsx @@ -0,0 +1 @@ +export { default as QuoteStore } from "./QuoteStore"; diff --git a/03_source/mobile/src/pages/DemoReactQuotes/theme/variables.css b/03_source/mobile/src/pages/DemoReactQuotes/theme/variables.css new file mode 100644 index 0000000..088e83c --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactQuotes/theme/variables.css @@ -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; +} \ No newline at end of file