"chore: add barcode scanner and clipboard plugins, update dev script to use yarn, and add new demo pages"

This commit is contained in:
louiscklaw
2025-06-04 17:48:04 +08:00
parent dff07ddcb0
commit d76d43d17f
124 changed files with 5977 additions and 7022 deletions

View 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;

View 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 } />
&nbsp;{ bookmarked ? "Bookmarked" : "Save as Bookmark" }
</IonButton>
</IonCol>
<IonCol size="4">
<IonButton fill="outline" onClick={ copyQuote }>
<IonIcon icon={ copyOutline } />
&nbsp;Copy Quote
</IonButton>
</IonCol>
</IonRow>
</IonCard>
</IonContent>
</IonPage>
);
};
export default Quote;

View 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;

View 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);
}

View 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;

View File

@@ -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>
);
}

View File

@@ -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;
}

View 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;

View 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 });
}

View 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]);

View File

@@ -0,0 +1 @@
export { default as QuoteStore } from "./QuoteStore";

View 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;
}