"chore: add barcode scanner and clipboard plugins, update dev script to use yarn, and add new demo pages"
@@ -9,6 +9,8 @@ android {
|
||||
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
implementation project(':capacitor-barcode-scanner')
|
||||
implementation project(':capacitor-clipboard')
|
||||
implementation project(':capacitor-geolocation')
|
||||
implementation project(':capacitor-preferences')
|
||||
|
||||
|
@@ -2,6 +2,12 @@
|
||||
include ':capacitor-android'
|
||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||
|
||||
include ':capacitor-barcode-scanner'
|
||||
project(':capacitor-barcode-scanner').projectDir = new File('../node_modules/@capacitor/barcode-scanner/android')
|
||||
|
||||
include ':capacitor-clipboard'
|
||||
project(':capacitor-clipboard').projectDir = new File('../node_modules/@capacitor/clipboard/android')
|
||||
|
||||
include ':capacitor-geolocation'
|
||||
project(':capacitor-geolocation').projectDir = new File('../node_modules/@capacitor/geolocation/android')
|
||||
|
||||
|
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
while true; do
|
||||
npm i -D
|
||||
yarn -D
|
||||
|
||||
npm run dev
|
||||
yarn run dev
|
||||
|
||||
echo "restarting..."
|
||||
sleep 1
|
||||
|
@@ -11,6 +11,8 @@ install! 'cocoapods', :disable_input_output_paths => true
|
||||
def capacitor_pods
|
||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorBarcodeScanner', :path => '../../node_modules/@capacitor/barcode-scanner'
|
||||
pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard'
|
||||
pod 'CapacitorGeolocation', :path => '../../node_modules/@capacitor/geolocation'
|
||||
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
|
||||
end
|
||||
|
6984
03_source/mobile/package-lock.json
generated
@@ -7,6 +7,8 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@capacitor/android": "7.0.1",
|
||||
"@capacitor/barcode-scanner": "^2.0.3",
|
||||
"@capacitor/clipboard": "^7.0.1",
|
||||
"@capacitor/core": "^7.0.0",
|
||||
"@capacitor/geolocation": "^7.1.2",
|
||||
"@capacitor/ios": "7.0.1",
|
||||
@@ -15,18 +17,22 @@
|
||||
"@ionic/react": "^8.5.0",
|
||||
"@ionic/react-router": "^8.5.0",
|
||||
"@mdx-js/react": "^3.1.0",
|
||||
"@react-hook/window-size": "^3.1.1",
|
||||
"@types/leaflet": "^1.9.17",
|
||||
"@types/react-redux": "^7.1.34",
|
||||
"axios": "^1.9.0",
|
||||
"date-fns": "^2.25.0",
|
||||
"ionicons": "^7.1.2",
|
||||
"leaflet": "^1.9.4",
|
||||
"pullstate": "^2.0.0-pre.0",
|
||||
"pullstate": "^1",
|
||||
"react": "19.0.0",
|
||||
"react-confetti": "^6.4.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-hook-form": "^7.55.0",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-qr-code": "^2.0.15",
|
||||
"react-qr-reader": "^3.0.0-beta-1",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^5.3.4",
|
||||
"react-router-dom": "^5.3.4",
|
||||
@@ -35,6 +41,7 @@
|
||||
"reselect": "^4.0.0",
|
||||
"sass": "^1.85.1",
|
||||
"swiper": "^9.1.1",
|
||||
"use-sound": "^5.0.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"scripts": {
|
||||
|
BIN
03_source/mobile/public/assets/DemoShopAppUi/cart.png
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
03_source/mobile/public/assets/DemoShopAppUi/icon/favicon.png
Normal file
After Width: | Height: | Size: 930 B |
BIN
03_source/mobile/public/assets/DemoShopAppUi/icon/icon.png
Normal file
After Width: | Height: | Size: 23 KiB |
1
03_source/mobile/public/assets/DemoShopAppUi/shapes.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="350" height="140" xmlns="http://www.w3.org/2000/svg" style="background:#f6f7f9"><g fill="none" fill-rule="evenodd"><path fill="#F04141" style="mix-blend-mode:multiply" d="M61.905-34.23l96.194 54.51-66.982 54.512L22 34.887z"/><circle fill="#10DC60" style="mix-blend-mode:multiply" cx="155.5" cy="135.5" r="57.5"/><path fill="#3880FF" style="mix-blend-mode:multiply" d="M208.538 9.513l84.417 15.392L223.93 93.93z"/><path fill="#FFCE00" style="mix-blend-mode:multiply" d="M268.625 106.557l46.332-26.75 46.332 26.75v53.5l-46.332 26.75-46.332-26.75z"/><circle fill="#7044FF" style="mix-blend-mode:multiply" cx="299.5" cy="9.5" r="38.5"/><rect fill="#11D3EA" style="mix-blend-mode:multiply" transform="rotate(-60 148.47 37.886)" x="143.372" y="-7.056" width="10.196" height="89.884" rx="5.098"/><path d="M-25.389 74.253l84.86 8.107c5.498.525 9.53 5.407 9.004 10.905a10 10 0 0 1-.057.477l-12.36 85.671a10.002 10.002 0 0 1-11.634 8.42l-86.351-15.226c-5.44-.959-9.07-6.145-8.112-11.584l13.851-78.551a10 10 0 0 1 10.799-8.219z" fill="#7044FF" style="mix-blend-mode:multiply"/><circle fill="#0CD1E8" style="mix-blend-mode:multiply" cx="273.5" cy="106.5" r="20.5"/></g></svg>
|
After Width: | Height: | Size: 1.1 KiB |
BIN
03_source/mobile/public/assets/DemoShopAppUi/shop.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
03_source/mobile/public/assets/ScoreBoard/icon/favicon.png
Normal file
After Width: | Height: | Size: 930 B |
BIN
03_source/mobile/public/assets/ScoreBoard/icon/icon.png
Normal file
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 682 KiB |
After Width: | Height: | Size: 820 KiB |
BIN
03_source/mobile/public/assets/ScoreBoard/scoreboardheader.jpeg
Normal file
After Width: | Height: | Size: 49 KiB |
1
03_source/mobile/public/assets/ScoreBoard/shapes.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="350" height="140" xmlns="http://www.w3.org/2000/svg" style="background:#f6f7f9"><g fill="none" fill-rule="evenodd"><path fill="#F04141" style="mix-blend-mode:multiply" d="M61.905-34.23l96.194 54.51-66.982 54.512L22 34.887z"/><circle fill="#10DC60" style="mix-blend-mode:multiply" cx="155.5" cy="135.5" r="57.5"/><path fill="#3880FF" style="mix-blend-mode:multiply" d="M208.538 9.513l84.417 15.392L223.93 93.93z"/><path fill="#FFCE00" style="mix-blend-mode:multiply" d="M268.625 106.557l46.332-26.75 46.332 26.75v53.5l-46.332 26.75-46.332-26.75z"/><circle fill="#7044FF" style="mix-blend-mode:multiply" cx="299.5" cy="9.5" r="38.5"/><rect fill="#11D3EA" style="mix-blend-mode:multiply" transform="rotate(-60 148.47 37.886)" x="143.372" y="-7.056" width="10.196" height="89.884" rx="5.098"/><path d="M-25.389 74.253l84.86 8.107c5.498.525 9.53 5.407 9.004 10.905a10 10 0 0 1-.057.477l-12.36 85.671a10.002 10.002 0 0 1-11.634 8.42l-86.351-15.226c-5.44-.959-9.07-6.145-8.112-11.584l13.851-78.551a10 10 0 0 1 10.799-8.219z" fill="#7044FF" style="mix-blend-mode:multiply"/><circle fill="#0CD1E8" style="mix-blend-mode:multiply" cx="273.5" cy="106.5" r="20.5"/></g></svg>
|
After Width: | Height: | Size: 1.1 KiB |
@@ -64,8 +64,17 @@ import ServiceAgreement from './pages/ServiceAgreement';
|
||||
import paths from './paths';
|
||||
import PrivacyAgreement from './pages/PrivacyAgreement';
|
||||
import AppRoute from './AppRoute';
|
||||
//
|
||||
import DemoReactShop from './pages/DemoReactShop';
|
||||
import DemoWeatherApp from './pages/WeatherDemo';
|
||||
import DemoClubHouse from './pages/DemoClubHouse';
|
||||
import DemoScoreBoard from './pages/DemoScoreBoard';
|
||||
import DemoQuoteApp from './pages/DemoQuoteApp';
|
||||
import DemoQrScanner from './pages/DemoQrScanner';
|
||||
// DemoShopAppUi
|
||||
import DemoShopAppUi from './pages/DemoShopAppUi';
|
||||
// DemoDictionaryApp
|
||||
import DemoDictionaryApp from './pages/DemoDictionaryApp';
|
||||
|
||||
setupIonicReact();
|
||||
|
||||
@@ -125,6 +134,12 @@ const IonicApp: React.FC<IonicAppProps> = ({
|
||||
<Route path="/tabs" render={() => <MainTabs />} />
|
||||
<Route path={paths.DEMO_REACT_SHOP} render={() => <DemoReactShop />} />
|
||||
<Route path={paths.DEMO_WEATHER_APP} render={() => <DemoWeatherApp />} />
|
||||
<Route path={paths.DEMO_CLUB_HOUSE} render={() => <DemoClubHouse />} />
|
||||
<Route path={paths.DEMO_SCORE_BOARD} render={() => <DemoScoreBoard />} />
|
||||
<Route path={paths.DEMO_QUOTE_APP} render={() => <DemoQuoteApp />} />
|
||||
<Route path={paths.DEMO_QR_SCANNER} render={() => <DemoQrScanner />} />
|
||||
<Route path={paths.DEMO_SHOP_APP_UI} render={() => <DemoShopAppUi />} />
|
||||
<Route path={paths.DEMO_DICTIONARY_APP} render={() => <DemoDictionaryApp />} />
|
||||
|
||||
{/* */}
|
||||
<Route path="/account" component={Account} />
|
||||
|
@@ -32,9 +32,6 @@ const AppRoute: React.FC = () => {
|
||||
<Route exact={true} path={paths.CHANGE_LANGUAGE} component={ChangeLanguage} />
|
||||
<Route exact={true} path={paths.SERVICE_AGREEMENT} component={ServiceAgreement} />
|
||||
<Route exact={true} path={paths.PRIVACY_AGREEMENT} component={PrivacyAgreement} />
|
||||
|
||||
{/* TODO: review DemoReactShop to fix */}
|
||||
{/* <Route path={paths.DEMO_REACT_SHOP} render={() => <DemoReactShop />} exact={true} /> */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -108,7 +108,6 @@ export const checkUserSession = () => async (dispatch: React.Dispatch<any>) => {
|
||||
|
||||
export const setHasSeenTutorial =
|
||||
(hasSeenTutorial: boolean) => async (dispatch: React.Dispatch<any>) => {
|
||||
debugger;
|
||||
await setHasSeenTutorialData(hasSeenTutorial);
|
||||
return {
|
||||
type: 'set-has-seen-tutorial',
|
||||
|
12
03_source/mobile/src/pages/DemoClubHouse/AppPages/Tab1.css
Normal file
@@ -0,0 +1,12 @@
|
||||
.extra-padding {
|
||||
|
||||
padding-right: 1.3rem !important;
|
||||
padding-left: 1.3rem !important;
|
||||
}
|
||||
|
||||
.title {
|
||||
|
||||
font-weight: 600;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
90
03_source/mobile/src/pages/DemoClubHouse/AppPages/Tab1.jsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonGrid,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonText,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonRouter,
|
||||
} from '@ionic/react';
|
||||
import { chevronBack, personOutline } from 'ionicons/icons';
|
||||
import { useStoreState } from 'pullstate';
|
||||
import { TalkStore } from '../store';
|
||||
import { getTalks } from '../store/Selectors';
|
||||
import './Tab1.css';
|
||||
|
||||
import { TalkCard } from '../components/TalkCard';
|
||||
import { useRef } from 'react';
|
||||
|
||||
const Tab1 = () => {
|
||||
const pageRef = useRef();
|
||||
const talks = useStoreState(TalkStore, getTalks);
|
||||
|
||||
const router = useIonRouter();
|
||||
function handleBackClick() {
|
||||
router.goBack();
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage ref={pageRef}>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>All Rooms</IonTitle>
|
||||
|
||||
<IonButtons slot="end">
|
||||
<IonButton>
|
||||
<IonIcon icon={personOutline} />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
|
||||
<IonButtons slot="start">
|
||||
<IonButton onclick={handleBackClick}>
|
||||
<IonIcon icon={chevronBack} />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonGrid className="ion-padding-start ion-padding-end extra-padding ion-padding-bottom ion-margin-bottom">
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<IonText color="dark">
|
||||
<p className="title">Upcoming</p>
|
||||
</IonText>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<TalkCard upcoming={true} talk={talks[0]} pageRef={pageRef} />
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<IonText color="dark">
|
||||
<p className="title">Happening Now</p>
|
||||
</IonText>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
{talks.map((talk, talkIndex) => {
|
||||
return talkIndex > 0 && <TalkCard key={talkIndex} talk={talk} pageRef={pageRef} />;
|
||||
})}
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tab1;
|
23
03_source/mobile/src/pages/DemoClubHouse/AppPages/Tab2.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
|
||||
import './Tab2.css';
|
||||
|
||||
const Tab2 = () => {
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>TO DO :)</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">TO DO :)</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tab2;
|
23
03_source/mobile/src/pages/DemoClubHouse/AppPages/Tab3.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
|
||||
import './Tab3.css';
|
||||
|
||||
const Tab3 = () => {
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>TO DO :)</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">TO DO :)</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tab3;
|
100
03_source/mobile/src/pages/DemoClubHouse/components/TalkCard.jsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { IonCardSubtitle, IonIcon, IonModal, IonNote, IonRow, useIonModal } from "@ionic/react";
|
||||
import { bulb, micOutline, personOutline } from "ionicons/icons";
|
||||
import { useStoreState } from "pullstate";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { CategoryStore } from '../store';
|
||||
import { getPeople } from "../store/PeopleStore";
|
||||
import { getCategory } from '../store/Selectors';
|
||||
|
||||
import styles from "./TalkCard.module.css";
|
||||
import { TalkModal } from "./TalkModal";
|
||||
|
||||
export const TalkCard = ({ upcoming = false, talk, pageRef }) => {
|
||||
|
||||
const talkCategory = useStoreState(CategoryStore, getCategory(talk.category_id));
|
||||
const [ speakers, setSpeakers ] = useState([]);
|
||||
const [ showModal, setShowModal ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
setSpeakers(getPeople(talk.speakers));
|
||||
}, [ talk ]);
|
||||
|
||||
// const [ present, dismiss ] = useIonModal(TalkModal, {
|
||||
|
||||
// dismiss: () => dismiss(),
|
||||
// talk,
|
||||
// speakers,
|
||||
// category: talkCategory
|
||||
// });
|
||||
|
||||
// const handleShowTalk = () => {
|
||||
|
||||
// console.log("in here");
|
||||
|
||||
// present({
|
||||
|
||||
// // presentingElement: pageRef.current
|
||||
// });
|
||||
// }
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={ `${ styles.talkCard } ${ upcoming && styles.upcomingCard }` } onClick={ () => setShowModal(true) }>
|
||||
<div className={ styles.cardTitle }>
|
||||
<IonIcon color={ upcoming ? "primary" : "white" } icon={ bulb } />
|
||||
<IonCardSubtitle color={ upcoming ? "light" : "primary" }>{ talkCategory.name } talks</IonCardSubtitle>
|
||||
</div>
|
||||
|
||||
<div className={ styles.talkTitle }>
|
||||
<h3>{ talk.title }</h3>
|
||||
</div>
|
||||
|
||||
{ !upcoming &&
|
||||
|
||||
<IonRow className={ styles.talkSpeakers }>
|
||||
{ speakers.map((speaker, index) => {
|
||||
|
||||
return (
|
||||
|
||||
<div key={ `speaker_${ index }` } className={ styles.talkSpeaker }>
|
||||
<img src={ speaker.image } alt="avatar" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</IonRow>
|
||||
}
|
||||
|
||||
{ upcoming &&
|
||||
<div className={ styles.talkDate }>
|
||||
<IonNote color="secondary">{ talk.time }</IonNote>
|
||||
<div className={ styles.detailCount }>
|
||||
<IonIcon icon={ micOutline } color="light " />
|
||||
<span>{ talk.speakers } Speakers</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{ !upcoming &&
|
||||
|
||||
<div className={ styles.talkDetails }>
|
||||
<div className={ styles.detailCount }>
|
||||
<IonIcon icon={ micOutline } color="primary" />
|
||||
<span>{ talk.speakers } Speakers</span>
|
||||
</div>
|
||||
|
||||
<div className={ styles.detailCount }>
|
||||
<IonIcon icon={ personOutline } color="primary" />
|
||||
<span>{ talk.audience } Audience</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<IonModal isOpen={ showModal } onDidDismiss={ () => setShowModal(false) } presentingElement={ pageRef.current }>
|
||||
<TalkModal dismiss={ () => setShowModal(false) } speakers={ speakers } talk={ talk } category={ talkCategory } />
|
||||
</IonModal>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,105 @@
|
||||
.talkCard {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 10px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.1);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.talkCard:not(:first-child) {
|
||||
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.upcomingCard {
|
||||
|
||||
background-color: var(--ion-color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cardTitle ion-icon {
|
||||
|
||||
border-radius: 500px;
|
||||
padding: 0.2rem;
|
||||
margin-right: 0.75rem;
|
||||
background-color: var(--ion-color-primary);
|
||||
margin-top: -0.2rem;
|
||||
}
|
||||
|
||||
.upcomingCard .cardTitle ion-icon {
|
||||
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.talkTitle h3 {
|
||||
|
||||
font-size: 1.3rem !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.talkDate {
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.talkSpeakers {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.talkSpeaker {
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 3.5rem;
|
||||
width: 3.5rem;
|
||||
border-radius: 12px;
|
||||
margin-right: 0.2rem;
|
||||
background-color: var(--ion-color-primary);
|
||||
}
|
||||
|
||||
.talkSpeaker img {
|
||||
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
.talkDetails {
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.detailCount {
|
||||
|
||||
display: flex;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.detailCount ion-icon {
|
||||
|
||||
font-size: 1.2rem;
|
||||
margin-right: 0.3rem;
|
||||
}
|
@@ -0,0 +1,98 @@
|
||||
import { IonButton, IonButtons, IonCardSubtitle, IonCol, IonContent, IonGrid, IonHeader, IonIcon, IonPage, IonRow, IonTitle, IonToolbar } from "@ionic/react";
|
||||
import { bulb, exitOutline, micOutline, personOutline } from "ionicons/icons";
|
||||
import { useStoreState } from "pullstate";
|
||||
import { PeopleStore } from "../store";
|
||||
import { getAllPeople } from "../store/Selectors";
|
||||
|
||||
import styles from "./TalkModal.module.css";
|
||||
|
||||
export const TalkModal = ({ dismiss, talk, category, speakers }) => {
|
||||
|
||||
const people = useStoreState(PeopleStore, getAllPeople);
|
||||
|
||||
return (
|
||||
|
||||
<IonPage className="talk-modal">
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Talk Room</IonTitle>
|
||||
|
||||
<IonButtons slot="end">
|
||||
<IonButton color="primary" onClick={ dismiss }>
|
||||
<IonIcon icon={ exitOutline } />
|
||||
{/* Leave Room */}
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent className={ styles.modal }>
|
||||
<IonGrid className="ion-padding-start ion-padding-end ion-margin-start ion-margin-end">
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<div className={ styles.cardTitle }>
|
||||
<IonIcon color="white" icon={ bulb } />
|
||||
<IonCardSubtitle color="primary">{ category.name } talks</IonCardSubtitle>
|
||||
</div>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow className={ styles.talkTitle }>
|
||||
<IonCol size="12">
|
||||
<h1>{ talk.title }</h1>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<div className={ styles.detailCount }>
|
||||
<IonIcon icon={ micOutline } color="primary" />
|
||||
<span>{ talk.speakers } Speakers</span>
|
||||
</div>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow className={ styles.talkSpeakers }>
|
||||
{ speakers.map((speaker, index) => {
|
||||
|
||||
return (
|
||||
|
||||
<IonCol className={ styles.speakerContainer }>
|
||||
<div key={ `speaker_${ index }` } className={ `${ styles.talkSpeaker } ${ index === 0 && styles.activeSpeaker }` }>
|
||||
<img src={ speaker.image } alt="avatar" />
|
||||
</div>
|
||||
<p>{ speaker.name.split(" ")[0] }</p>
|
||||
</IonCol>
|
||||
);
|
||||
})}
|
||||
</IonRow>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<div className={ styles.detailCount }>
|
||||
<IonIcon icon={ personOutline } color="primary" />
|
||||
<span>{ talk.audience } Audience</span>
|
||||
</div>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow className={ styles.talkSpeakers }>
|
||||
{ [ ...Array(talk.audience)].map((audience, index) => {
|
||||
|
||||
return (
|
||||
|
||||
<IonCol size="3" className={ `${ styles.speakerContainer } ${ styles.audienceContainer }` }>
|
||||
<div key={ `speaker_${ index }` } className={ styles.talkSpeaker }>
|
||||
<img src={ people[Math.floor(Math.random() * 30)].image } alt="avatar" />
|
||||
</div>
|
||||
<p>{ people[Math.floor(Math.random() * 30)].name.split(" ")[0] }</p>
|
||||
</IonCol>
|
||||
);
|
||||
})}
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
@@ -0,0 +1,150 @@
|
||||
.talkCard {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 10px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.1);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.talkCard:not(:first-child) {
|
||||
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.upcomingCard {
|
||||
|
||||
background-color: var(--ion-color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cardTitle ion-icon {
|
||||
|
||||
border-radius: 500px;
|
||||
padding: 0.2rem;
|
||||
margin-right: 0.75rem;
|
||||
background-color: var(--ion-color-primary);
|
||||
margin-top: -0.2rem;
|
||||
}
|
||||
|
||||
.upcomingCard .cardTitle ion-icon {
|
||||
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.talkTitle {
|
||||
|
||||
margin-top: -1rem;
|
||||
}
|
||||
|
||||
.talkTitle h3 {
|
||||
|
||||
font-size: 1.3rem !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.talkDate {
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.talkSpeakers {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.speakerContainer {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.audienceContainer {
|
||||
|
||||
margin-bottom: 1rem;
|
||||
/* margin: 0.5rem; */
|
||||
}
|
||||
|
||||
.talkSpeaker {
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 3.5rem;
|
||||
width: 3.5rem;
|
||||
border-radius: 12px;
|
||||
margin-right: 0.2rem;
|
||||
background-color: var(--ion-color-primary);
|
||||
}
|
||||
|
||||
.audienceContainer .talkSpeaker {
|
||||
|
||||
background-color: #f2efe5;
|
||||
border: 2px solid #dfd9c7;
|
||||
}
|
||||
|
||||
.talkSpeaker img {
|
||||
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
.speakerContainer p {
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-top: 0.2rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.talkDetails {
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.detailCount {
|
||||
|
||||
display: flex;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.detailCount ion-icon {
|
||||
|
||||
font-size: 1.2rem;
|
||||
margin-right: 0.3rem;
|
||||
margin-left: -0.4rem;
|
||||
}
|
||||
|
||||
.activeSpeaker {
|
||||
|
||||
border: 3px solid rgb(255, 187, 0);
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
import { IonCardSubtitle, IonCol, IonIcon, IonNote, IonRow } from '@ionic/react';
|
||||
import { pulseOutline, sunnyOutline, thermometerOutline } from 'ionicons/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const WeatherProperty = ({ type, currentWeather }: { type: any; currentWeather: any }) => {
|
||||
const [property, setProperty] = useState(false);
|
||||
|
||||
const properties = {
|
||||
wind: {
|
||||
isIcon: false,
|
||||
icon: '/assets/WeatherDemo/wind.png',
|
||||
alt: 'wind',
|
||||
label: 'Wind',
|
||||
value: `${currentWeather.current.wind_mph}mph`,
|
||||
},
|
||||
feelsLike: {
|
||||
isIcon: true,
|
||||
icon: thermometerOutline,
|
||||
alt: 'feels like',
|
||||
label: 'Feels like',
|
||||
value: `${currentWeather.current.feelslike_c}°C`,
|
||||
},
|
||||
indexUV: {
|
||||
isIcon: true,
|
||||
icon: sunnyOutline,
|
||||
alt: 'index uv',
|
||||
label: 'Index UV',
|
||||
value: currentWeather.current.uv,
|
||||
},
|
||||
pressure: {
|
||||
isIcon: true,
|
||||
icon: pulseOutline,
|
||||
alt: 'pressure',
|
||||
label: 'Pressure',
|
||||
value: `${currentWeather.current.pressure_mb} mbar`,
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setProperty(properties[type]);
|
||||
}, [type]);
|
||||
|
||||
return (
|
||||
<IonCol size="6">
|
||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
||||
<IonCol size="3">
|
||||
{!property.isIcon && (
|
||||
<img alt={property.alt} src={property.icon} height="32" width="32" />
|
||||
)}
|
||||
{property.isIcon && (
|
||||
<IonIcon icon={property.icon} color="medium" style={{ fontSize: '2rem' }} />
|
||||
)}
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="9">
|
||||
<IonCardSubtitle>{property.label}</IonCardSubtitle>
|
||||
<IonNote>{property.value}</IonNote>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCol>
|
||||
);
|
||||
};
|
@@ -0,0 +1,48 @@
|
||||
import { IonCard, IonCardContent, IonGrid, IonRow, IonText, IonCardTitle } from '@ionic/react';
|
||||
import { WeatherProperty } from './WeatherProperty';
|
||||
|
||||
export const CurrentWeather = ({ currentWeather }: { currentWeather: any }) => (
|
||||
<IonGrid>
|
||||
<IonCard>
|
||||
<IonCardContent className="ion-text-center">
|
||||
<IonText color="primary">
|
||||
<h1>
|
||||
{currentWeather.location.region},{' '}
|
||||
<span style={{ color: 'gray' }}>{currentWeather.location.country}</span>
|
||||
</h1>
|
||||
</IonText>
|
||||
|
||||
<div className="ion-margin-top">
|
||||
<img
|
||||
alt="condition"
|
||||
src={currentWeather.current.condition.icon.replace('//', 'https://')}
|
||||
/>
|
||||
|
||||
<IonText color="dark">
|
||||
<h1 style={{ fontWeight: 'bold' }}>{currentWeather.current.condition.text}</h1>
|
||||
</IonText>
|
||||
|
||||
<IonText color="medium">
|
||||
<p>{new Date(currentWeather.location.localtime).toDateString()}</p>
|
||||
</IonText>
|
||||
</div>
|
||||
|
||||
<IonCardTitle style={{ fontSize: '3rem' }} className="ion-margin-top">
|
||||
{currentWeather.current.temp_c}℃
|
||||
</IonCardTitle>
|
||||
|
||||
<IonGrid className="ion-margin-top">
|
||||
<IonRow>
|
||||
<WeatherProperty type="wind" currentWeather={currentWeather} />
|
||||
<WeatherProperty type="feelsLike" currentWeather={currentWeather} />
|
||||
</IonRow>
|
||||
|
||||
<IonRow className="ion-margin-top">
|
||||
<WeatherProperty type="indexUV" currentWeather={currentWeather} />
|
||||
<WeatherProperty type="pressure" currentWeather={currentWeather} />
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
</IonGrid>
|
||||
);
|
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
IonCard,
|
||||
IonCardContent,
|
||||
IonCardSubtitle,
|
||||
IonCardTitle,
|
||||
IonCol,
|
||||
IonGrid,
|
||||
IonIcon,
|
||||
IonNote,
|
||||
IonRow,
|
||||
IonSkeletonText,
|
||||
IonText,
|
||||
IonThumbnail,
|
||||
} from '@ionic/react';
|
||||
import { pulseOutline, sunnyOutline, thermometerOutline } from 'ionicons/icons';
|
||||
|
||||
export const SkeletonDashboard = () => (
|
||||
<IonGrid>
|
||||
<IonCard>
|
||||
<IonCardContent className="ion-text-center">
|
||||
<IonText color="primary">
|
||||
<h1>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</h1>
|
||||
</IonText>
|
||||
|
||||
<div className="ion-margin-top">
|
||||
<IonThumbnail>
|
||||
<IonSkeletonText animated style={{ width: '2rem', height: '2rem' }} />
|
||||
</IonThumbnail>
|
||||
|
||||
<IonText color="dark">
|
||||
<h1 style={{ fontWeight: 'bold' }}>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</h1>
|
||||
</IonText>
|
||||
|
||||
<IonText color="medium">
|
||||
<p>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</p>
|
||||
</IonText>
|
||||
</div>
|
||||
|
||||
<IonCardTitle style={{ fontSize: '3rem' }} className="ion-margin-top">
|
||||
<IonSkeletonText animated style={{ height: '3rem', width: '30%', textAlign: 'center' }} />
|
||||
</IonCardTitle>
|
||||
|
||||
<IonGrid className="ion-margin-top">
|
||||
<IonRow>
|
||||
<IonCol size="6">
|
||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
||||
<IonCol size="3">
|
||||
<img alt="wind" src="/assets/WeatherDemo/wind.png" height="32" width="32" />
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="9">
|
||||
<IonCardSubtitle>Wind</IonCardSubtitle>
|
||||
<IonNote>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</IonNote>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="6">
|
||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
||||
<IonCol size="3">
|
||||
<IonIcon icon={thermometerOutline} color="medium" style={{ fontSize: '2rem' }} />
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="9">
|
||||
<IonCardSubtitle>Feels like</IonCardSubtitle>
|
||||
<IonNote>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</IonNote>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow className="ion-margin-top">
|
||||
<IonCol size="6">
|
||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
||||
<IonCol size="3">
|
||||
<IonIcon icon={sunnyOutline} color="medium" style={{ fontSize: '2rem' }} />
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="9">
|
||||
<IonCardSubtitle>Index UV</IonCardSubtitle>
|
||||
<IonNote>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</IonNote>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="6">
|
||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
||||
<IonCol size="3">
|
||||
<IonIcon icon={pulseOutline} color="medium" style={{ fontSize: '2rem' }} />
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="9">
|
||||
<IonCardSubtitle>Pressure</IonCardSubtitle>
|
||||
<IonNote>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</IonNote>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
</IonGrid>
|
||||
);
|
116
03_source/mobile/src/pages/DemoClubHouse/index.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonLabel,
|
||||
IonPage,
|
||||
IonRouterOutlet,
|
||||
IonRow,
|
||||
IonTabBar,
|
||||
IonTabButton,
|
||||
IonTabs,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonRouter,
|
||||
} from '@ionic/react';
|
||||
|
||||
import { Geolocation } from '@capacitor/geolocation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SkeletonDashboard } from './components1/SkeletonDashboard';
|
||||
import {
|
||||
addCircle,
|
||||
addCircleOutline,
|
||||
chevronBack,
|
||||
cloudOutline,
|
||||
home,
|
||||
homeOutline,
|
||||
notifications,
|
||||
notificationsOutline,
|
||||
person,
|
||||
personOutline,
|
||||
refreshOutline,
|
||||
search,
|
||||
searchOutline,
|
||||
} from 'ionicons/icons';
|
||||
import { CurrentWeather } from './components1/CurrentWeather';
|
||||
import { IonReactRouter } from '@ionic/react-router';
|
||||
import { Route, Redirect } from 'react-router';
|
||||
import Tab1 from './AppPages/Tab1';
|
||||
import Tab2 from './AppPages/Tab2';
|
||||
import Tab3 from './AppPages/Tab3';
|
||||
|
||||
const DemoClubHouse = () => {
|
||||
const tabs = [
|
||||
{
|
||||
name: 'Home',
|
||||
url: '/demo-club-house/home',
|
||||
activeIcon: home,
|
||||
icon: homeOutline,
|
||||
component: Tab1,
|
||||
},
|
||||
{
|
||||
name: 'Search',
|
||||
url: '/search',
|
||||
activeIcon: search,
|
||||
icon: searchOutline,
|
||||
component: Tab2,
|
||||
},
|
||||
{
|
||||
name: 'Add',
|
||||
url: '/add',
|
||||
activeIcon: addCircle,
|
||||
icon: addCircleOutline,
|
||||
component: Tab3,
|
||||
},
|
||||
{
|
||||
name: 'Account',
|
||||
url: '/account',
|
||||
activeIcon: person,
|
||||
icon: personOutline,
|
||||
component: Tab3,
|
||||
},
|
||||
{
|
||||
name: 'Notifications',
|
||||
url: '/notifications',
|
||||
activeIcon: notifications,
|
||||
icon: notificationsOutline,
|
||||
component: Tab3,
|
||||
},
|
||||
];
|
||||
|
||||
const [activeTab, setActiveTab] = useState(tabs[0].name);
|
||||
|
||||
return (
|
||||
<IonTabs onIonTabsDidChange={(e) => setActiveTab(e.detail.tab)}>
|
||||
<IonRouterOutlet>
|
||||
{tabs.map((tab, index) => {
|
||||
return (
|
||||
<Route key={index} exact path={tab.url}>
|
||||
<tab.component />
|
||||
</Route>
|
||||
);
|
||||
})}
|
||||
|
||||
<Route exact path="/demo-club-house">
|
||||
<Redirect to="/demo-club-house/home" />
|
||||
</Route>
|
||||
</IonRouterOutlet>
|
||||
<IonTabBar slot="bottom">
|
||||
{tabs.map((tab, barIndex) => {
|
||||
const active = tab.name === activeTab;
|
||||
|
||||
return (
|
||||
<IonTabButton key={`tab_${barIndex}`} tab={tab.name} href={tab.url}>
|
||||
<IonIcon icon={active ? tab.activeIcon : tab.icon} />
|
||||
</IonTabButton>
|
||||
);
|
||||
})}
|
||||
</IonTabBar>
|
||||
</IonTabs>
|
||||
);
|
||||
};
|
||||
|
||||
export default DemoClubHouse;
|
@@ -0,0 +1,25 @@
|
||||
import { Store } from "pullstate";
|
||||
|
||||
const CategoryStore = new Store({
|
||||
|
||||
categories: [
|
||||
{
|
||||
id: 1,
|
||||
name: "Design"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Javascript"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Mobile"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Business"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default CategoryStore;
|
209
03_source/mobile/src/pages/DemoClubHouse/store/PeopleStore.js
Normal file
@@ -0,0 +1,209 @@
|
||||
import { Store } from "pullstate";
|
||||
|
||||
const PeopleStore = new Store({
|
||||
|
||||
people: [
|
||||
{
|
||||
name: "Andrew Bennet",
|
||||
image: "/avatars/Avatar-1.png"
|
||||
},
|
||||
{
|
||||
name: "Elizabeth Moore",
|
||||
image: "/avatars/Avatar-2.png"
|
||||
},
|
||||
{
|
||||
name: "Oscar Clarke",
|
||||
image: "/avatars/Avatar-3.png"
|
||||
},
|
||||
{
|
||||
name: "Sandra Simpson",
|
||||
image: "/avatars/Avatar-4.png"
|
||||
},
|
||||
{
|
||||
name: "Sophia Price",
|
||||
image: "/avatars/Avatar-5.png"
|
||||
},
|
||||
{
|
||||
name: "Jasmine Ruiz",
|
||||
image: "/avatars/Avatar-6.png"
|
||||
},
|
||||
{
|
||||
name: "Adriana Bonny",
|
||||
image: "/avatars/Avatar-7.png"
|
||||
},
|
||||
{
|
||||
name: "Maya Watson",
|
||||
image: "/avatars/Avatar-8.png"
|
||||
},
|
||||
{
|
||||
name: "Tatum Porter",
|
||||
image: "/avatars/Avatar-9.png"
|
||||
},
|
||||
{
|
||||
name: "Jackson Watts",
|
||||
image: "/avatars/Avatar-10.png"
|
||||
},
|
||||
{
|
||||
name: "Lana Cooper",
|
||||
image: "/avatars/Avatar-11.png"
|
||||
},
|
||||
{
|
||||
name: "Mateo Hoffman",
|
||||
image: "/avatars/Avatar-12.png"
|
||||
},
|
||||
{
|
||||
name: "Harper James",
|
||||
image: "/avatars/Avatar-13.png"
|
||||
},
|
||||
{
|
||||
name: "Edgar Douglas",
|
||||
image: "/avatars/Avatar-14.png"
|
||||
},
|
||||
{
|
||||
name: "Lilly Hale",
|
||||
image: "/avatars/Avatar-15.png"
|
||||
},
|
||||
{
|
||||
name: "Jade Williams",
|
||||
image: "/avatars/Avatar-16.png"
|
||||
},
|
||||
{
|
||||
name: "Cayden Long",
|
||||
image: "/avatars/Avatar-17.png"
|
||||
},
|
||||
{
|
||||
name: "Millie Klein",
|
||||
image: "/avatars/Avatar-18.png"
|
||||
},
|
||||
{
|
||||
name: "Heidi Toffer",
|
||||
image: "/avatars/Avatar-19.png"
|
||||
},
|
||||
{
|
||||
name: "Alaya Bailey",
|
||||
image: "/avatars/Avatar-20.png"
|
||||
},
|
||||
{
|
||||
name: "Laura Diaz",
|
||||
image: "/avatars/Avatar-21.png"
|
||||
},
|
||||
{
|
||||
name: "Alina Gomez",
|
||||
image: "/avatars/Avatar-22.png"
|
||||
},
|
||||
{
|
||||
name: "Rachel Tiffin",
|
||||
image: "/avatars/Avatar-23.png"
|
||||
},
|
||||
{
|
||||
name: "Liz Faxty",
|
||||
image: "/avatars/Avatar-24.png"
|
||||
},
|
||||
{
|
||||
name: "Sarah Goodman",
|
||||
image: "/avatars/Avatar-25.png"
|
||||
},
|
||||
{
|
||||
name: "Melissa Bengin",
|
||||
image: "/avatars/Avatar-26.png"
|
||||
},
|
||||
{
|
||||
name: "Stephanie Morter",
|
||||
image: "/avatars/Avatar-27.png"
|
||||
},
|
||||
{
|
||||
name: "Rebecca Slims",
|
||||
image: "/avatars/Avatar-28.png"
|
||||
},
|
||||
{
|
||||
name: "Arielle May",
|
||||
image: "/avatars/Avatar-29.png"
|
||||
},
|
||||
{
|
||||
name: "Jack Boppes",
|
||||
image: "/avatars/Avatar-30.png"
|
||||
},
|
||||
{
|
||||
name: "Christina Rankin",
|
||||
image: "/avatars/Avatar-31.png"
|
||||
},
|
||||
{
|
||||
name: "Ronan Murf",
|
||||
image: "/avatars/Avatar-32.png"
|
||||
},
|
||||
{
|
||||
name: "Daniel Jackson",
|
||||
image: "/avatars/Avatar-33.png"
|
||||
},
|
||||
{
|
||||
name: "Richard Bales",
|
||||
image: "/avatars/Avatar-34.png"
|
||||
},
|
||||
{
|
||||
name: "Harmony Martin",
|
||||
image: "/avatars/Avatar-35.png"
|
||||
},
|
||||
{
|
||||
name: "Chris Huges",
|
||||
image: "/avatars/Avatar-36.png"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default PeopleStore;
|
||||
|
||||
export const getPeople = amount => {
|
||||
|
||||
let tempPeople = [ ...PeopleStore.getRawState().people ];
|
||||
return tempPeople.sort(() => Math.random() - Math.random()).slice(0, amount);
|
||||
}
|
||||
|
||||
// export const markActiveAsDone = () => {
|
||||
|
||||
// PeopleStore.update(state => {
|
||||
|
||||
// const scoreboardIndex = state.scoreboards.findIndex(scoreboard => scoreboard.active === true);
|
||||
// state.scoreboards[scoreboardIndex].done = true;
|
||||
// });
|
||||
// }
|
||||
|
||||
// export const addScoreboard = (players, details) => {
|
||||
|
||||
// PeopleStore.update(s => { s.scoreboards = s.scoreboards.map(scoreboard => scoreboard.active = false) });
|
||||
|
||||
// PeopleStore.update(state => {
|
||||
|
||||
// state.scoreboards.forEach((scoreboard, index) => {
|
||||
|
||||
// state.scoreboards[index].active = false;
|
||||
// });
|
||||
// });
|
||||
|
||||
|
||||
// const newScoreboard = {
|
||||
|
||||
// id: Date.now(),
|
||||
// title: details.title,
|
||||
// players: [ ...players ],
|
||||
// active: true
|
||||
// };
|
||||
|
||||
// const playersToSave = players.filter(p => p.saved === true);
|
||||
|
||||
// PeopleStore.update(s => { s.scoreboards = [ ...s.scoreboards, newScoreboard ] });
|
||||
// PeopleStore.update(s => { s.players = [ ...s.players, ...playersToSave ] });
|
||||
// }
|
||||
|
||||
// export const addScoreToPlayer = (scoreboardId, playerIndex) => {
|
||||
|
||||
// PeopleStore.update(state => {
|
||||
|
||||
// const scoreboardIndex = state.scoreboards.findIndex(scoreboard => scoreboard.id === parseInt(scoreboardId));
|
||||
// state.scoreboards[scoreboardIndex].players[playerIndex].score += 1;
|
||||
|
||||
// state.scoreboards[scoreboardIndex].players.sort((a, b) => {
|
||||
// if (a.score > b.score) return -1
|
||||
// return a.score < b.score ? 1 : 0
|
||||
// });
|
||||
// });
|
||||
// }
|
13
03_source/mobile/src/pages/DemoClubHouse/store/Selectors.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
const getState = state => state;
|
||||
|
||||
// General getters
|
||||
export const getAllPeople = createSelector(getState, state => state.people);
|
||||
export const getCategories = createSelector(getState, state => state.categories);
|
||||
export const getTalks = createSelector(getState, state => state.talks);
|
||||
|
||||
// Specific getters
|
||||
export const getCategoryTalks = categoryId => createSelector(getState, state => state.talks.filter(talk => parseInt(talk.category_id) === parseInt(categoryId))[0]);
|
||||
export const getTalk = id => createSelector(getState, state => state.talks.filter(talk => parseInt(talk.id) === parseInt(id))[0]);
|
||||
export const getCategory = id => createSelector(getState, state => state.categories.filter(category => parseInt(category.id) === parseInt(id))[0]);
|
111
03_source/mobile/src/pages/DemoClubHouse/store/TalkStore.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import { Store } from "pullstate";
|
||||
|
||||
const TalkStore = new Store({
|
||||
|
||||
talks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "The future of design systems",
|
||||
date: "29th Oct 2021",
|
||||
time: "5:00PM",
|
||||
speakers: 3,
|
||||
audience: 14,
|
||||
category_id: 1
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Lets talk about ReactJS",
|
||||
date: "13th Nov 2021",
|
||||
time: "2:00PM",
|
||||
speakers: 4,
|
||||
audience: 239,
|
||||
category_id: 2
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "How Ionic can transform mobile development",
|
||||
date: "21st Nov 2021",
|
||||
time: "7:30PM",
|
||||
speakers: 2,
|
||||
audience: 371,
|
||||
category_id: 3
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Using capacitor to access native features",
|
||||
date: "25th Nov 2021",
|
||||
time: "4:15PM",
|
||||
speakers: 2,
|
||||
audience: 587,
|
||||
category_id: 3
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "Does SASS give you an advantage?",
|
||||
date: "29th Nov 2021",
|
||||
time: "6:00PM",
|
||||
speakers: 4,
|
||||
audience: 97,
|
||||
category_id: 1
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: "Building a startup from the ground up",
|
||||
date: "1st Dec 2021",
|
||||
time: "9:00PM",
|
||||
speakers: 4,
|
||||
audience: 316,
|
||||
category_id: 4
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: "How we went from 9-5 to my own boss",
|
||||
date: "12th Dec 2021",
|
||||
time: "1:00PM",
|
||||
speakers: 2,
|
||||
audience: 33,
|
||||
category_id: 4
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: "Features of the beast, Angular",
|
||||
date: "19th Dec 2021",
|
||||
time: "3:30PM",
|
||||
speakers: 3,
|
||||
audience: 114,
|
||||
category_id: 2
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default TalkStore;
|
||||
|
||||
export const addTalk = details => {
|
||||
|
||||
const newTalk = {
|
||||
|
||||
id: Date.now(),
|
||||
title: details.title,
|
||||
date: details.date,
|
||||
time: details.time,
|
||||
speakers: details.speakers,
|
||||
audience: Math.random(900),
|
||||
category_id: details.category_id
|
||||
};
|
||||
|
||||
TalkStore.update(s => { s.talks = [ ...s.talks, newTalk ] });
|
||||
}
|
||||
|
||||
// export const addScoreToPlayer = (scoreboardId, playerIndex) => {
|
||||
|
||||
// PeopleStore.update(state => {
|
||||
|
||||
// const scoreboardIndex = state.scoreboards.findIndex(scoreboard => scoreboard.id === parseInt(scoreboardId));
|
||||
// state.scoreboards[scoreboardIndex].players[playerIndex].score += 1;
|
||||
|
||||
// state.scoreboards[scoreboardIndex].players.sort((a, b) => {
|
||||
// if (a.score > b.score) return -1
|
||||
// return a.score < b.score ? 1 : 0
|
||||
// });
|
||||
// });
|
||||
// }
|
3
03_source/mobile/src/pages/DemoClubHouse/store/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as PeopleStore } from "./PeopleStore";
|
||||
export { default as TalkStore } from "./TalkStore";
|
||||
export { default as CategoryStore } from "./CategoryStore";
|
103
03_source/mobile/src/pages/DemoClubHouse/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/WeatherDemo/img/about/madison.jpg');
|
||||
}
|
||||
|
||||
.about-header .austin {
|
||||
background-image: url('/assets/WeatherDemo/img/about/austin.jpg');
|
||||
}
|
||||
|
||||
.about-header .chicago {
|
||||
background-image: url('/assets/WeatherDemo/img/about/chicago.jpg');
|
||||
}
|
||||
|
||||
.about-header .seattle {
|
||||
background-image: url('/assets/WeatherDemo/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;
|
||||
}
|
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonRouter,
|
||||
} from '@ionic/react';
|
||||
|
||||
import { Geolocation } from '@capacitor/geolocation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SkeletonDashboard } from '../components/SkeletonDashboard';
|
||||
import { chevronBackOutline, refreshOutline } from 'ionicons/icons';
|
||||
import { CurrentWeather } from '../components/CurrentWeather';
|
||||
|
||||
function Tab1() {
|
||||
const router = useIonRouter();
|
||||
|
||||
const [currentWeather, setCurrentWeather] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getCurrentPosition();
|
||||
}, []);
|
||||
|
||||
const getCurrentPosition = async () => {
|
||||
setCurrentWeather(false);
|
||||
const coordinates = await Geolocation.getCurrentPosition();
|
||||
getAddress(coordinates.coords);
|
||||
};
|
||||
|
||||
const getAddress = async (coords) => {
|
||||
const query = `${coords.latitude},${coords.longitude}`;
|
||||
const response = await fetch(
|
||||
`https://api.weatherapi.com/v1/current.json?key=f93eb660b2424258bf5155016210712&q=${query}`
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
setCurrentWeather(data);
|
||||
};
|
||||
|
||||
function handleBackClick() {
|
||||
router.goBack();
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>My Weather</IonTitle>
|
||||
|
||||
<IonButtons slot="end">
|
||||
<IonButton onClick={() => getCurrentPosition()}>
|
||||
<IonIcon icon={refreshOutline} color="primary" />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
|
||||
<IonButtons slot="start">
|
||||
<IonButton onClick={() => handleBackClick()}>
|
||||
<IonIcon icon={chevronBackOutline} color="primary" />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Dashboard</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonRow className="ion-margin-start ion-margin-end ion-justify-content-center ion-text-center">
|
||||
<IonCol size="12">
|
||||
<h4>Here's your location based weather</h4>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<div style={{ marginTop: '-1.5rem' }}>
|
||||
{currentWeather ? (
|
||||
<CurrentWeather currentWeather={currentWeather} />
|
||||
) : (
|
||||
<SkeletonDashboard />
|
||||
)}
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
|
||||
export default Tab1;
|
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonSearchbar,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
import { useState } from 'react';
|
||||
import { CurrentWeather } from '../components/CurrentWeather';
|
||||
|
||||
function Tab2() {
|
||||
const [search, setSearch] = useState('');
|
||||
const [currentWeather, setCurrentWeather] = useState(false);
|
||||
|
||||
const performSearch = async () => {
|
||||
getAddress(search);
|
||||
};
|
||||
|
||||
const getAddress = async (city) => {
|
||||
const response = await fetch(
|
||||
`https://api.weatherapi.com/v1/current.json?key=f93eb660b2424258bf5155016210712&q=${city}&aqi=no`
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (data && data.current && data.location) {
|
||||
setCurrentWeather(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Search</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Search</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonRow className="ion-justify-content-center ion-margin-top ion-align-items-center">
|
||||
<IonCol size="7">
|
||||
<IonSearchbar
|
||||
placeholder="Try 'London'"
|
||||
animated
|
||||
value={search}
|
||||
onIonChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="5">
|
||||
<IonButton
|
||||
expand="block"
|
||||
className="ion-margin-start ion-margin-end"
|
||||
onClick={performSearch}
|
||||
>
|
||||
Search
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<div style={{ marginTop: '-0.8rem' }}>
|
||||
{currentWeather ? (
|
||||
<CurrentWeather currentWeather={currentWeather} />
|
||||
) : (
|
||||
<h3 className="ion-text-center">Your search result will appear here</h3>
|
||||
)}
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
|
||||
export default Tab2;
|
@@ -0,0 +1,62 @@
|
||||
import { IonCardSubtitle, IonCol, IonIcon, IonNote, IonRow } from '@ionic/react';
|
||||
import { pulseOutline, sunnyOutline, thermometerOutline } from 'ionicons/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const WeatherProperty = ({ type, currentWeather }: { type: any; currentWeather: any }) => {
|
||||
const [property, setProperty] = useState(false);
|
||||
|
||||
const properties = {
|
||||
wind: {
|
||||
isIcon: false,
|
||||
icon: '/assets/WeatherDemo/wind.png',
|
||||
alt: 'wind',
|
||||
label: 'Wind',
|
||||
value: `${currentWeather.current.wind_mph}mph`,
|
||||
},
|
||||
feelsLike: {
|
||||
isIcon: true,
|
||||
icon: thermometerOutline,
|
||||
alt: 'feels like',
|
||||
label: 'Feels like',
|
||||
value: `${currentWeather.current.feelslike_c}°C`,
|
||||
},
|
||||
indexUV: {
|
||||
isIcon: true,
|
||||
icon: sunnyOutline,
|
||||
alt: 'index uv',
|
||||
label: 'Index UV',
|
||||
value: currentWeather.current.uv,
|
||||
},
|
||||
pressure: {
|
||||
isIcon: true,
|
||||
icon: pulseOutline,
|
||||
alt: 'pressure',
|
||||
label: 'Pressure',
|
||||
value: `${currentWeather.current.pressure_mb} mbar`,
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setProperty(properties[type]);
|
||||
}, [type]);
|
||||
|
||||
return (
|
||||
<IonCol size="6">
|
||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
||||
<IonCol size="3">
|
||||
{!property.isIcon && (
|
||||
<img alt={property.alt} src={property.icon} height="32" width="32" />
|
||||
)}
|
||||
{property.isIcon && (
|
||||
<IonIcon icon={property.icon} color="medium" style={{ fontSize: '2rem' }} />
|
||||
)}
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="9">
|
||||
<IonCardSubtitle>{property.label}</IonCardSubtitle>
|
||||
<IonNote>{property.value}</IonNote>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCol>
|
||||
);
|
||||
};
|
@@ -0,0 +1,48 @@
|
||||
import { IonCard, IonCardContent, IonGrid, IonRow, IonText, IonCardTitle } from '@ionic/react';
|
||||
import { WeatherProperty } from './WeatherProperty';
|
||||
|
||||
export const CurrentWeather = ({ currentWeather }: { currentWeather: any }) => (
|
||||
<IonGrid>
|
||||
<IonCard>
|
||||
<IonCardContent className="ion-text-center">
|
||||
<IonText color="primary">
|
||||
<h1>
|
||||
{currentWeather.location.region},{' '}
|
||||
<span style={{ color: 'gray' }}>{currentWeather.location.country}</span>
|
||||
</h1>
|
||||
</IonText>
|
||||
|
||||
<div className="ion-margin-top">
|
||||
<img
|
||||
alt="condition"
|
||||
src={currentWeather.current.condition.icon.replace('//', 'https://')}
|
||||
/>
|
||||
|
||||
<IonText color="dark">
|
||||
<h1 style={{ fontWeight: 'bold' }}>{currentWeather.current.condition.text}</h1>
|
||||
</IonText>
|
||||
|
||||
<IonText color="medium">
|
||||
<p>{new Date(currentWeather.location.localtime).toDateString()}</p>
|
||||
</IonText>
|
||||
</div>
|
||||
|
||||
<IonCardTitle style={{ fontSize: '3rem' }} className="ion-margin-top">
|
||||
{currentWeather.current.temp_c}℃
|
||||
</IonCardTitle>
|
||||
|
||||
<IonGrid className="ion-margin-top">
|
||||
<IonRow>
|
||||
<WeatherProperty type="wind" currentWeather={currentWeather} />
|
||||
<WeatherProperty type="feelsLike" currentWeather={currentWeather} />
|
||||
</IonRow>
|
||||
|
||||
<IonRow className="ion-margin-top">
|
||||
<WeatherProperty type="indexUV" currentWeather={currentWeather} />
|
||||
<WeatherProperty type="pressure" currentWeather={currentWeather} />
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
</IonGrid>
|
||||
);
|
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
IonCard,
|
||||
IonCardContent,
|
||||
IonCardSubtitle,
|
||||
IonCardTitle,
|
||||
IonCol,
|
||||
IonGrid,
|
||||
IonIcon,
|
||||
IonNote,
|
||||
IonRow,
|
||||
IonSkeletonText,
|
||||
IonText,
|
||||
IonThumbnail,
|
||||
} from '@ionic/react';
|
||||
import { pulseOutline, sunnyOutline, thermometerOutline } from 'ionicons/icons';
|
||||
|
||||
export const SkeletonDashboard = () => (
|
||||
<IonGrid>
|
||||
<IonCard>
|
||||
<IonCardContent className="ion-text-center">
|
||||
<IonText color="primary">
|
||||
<h1>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</h1>
|
||||
</IonText>
|
||||
|
||||
<div className="ion-margin-top">
|
||||
<IonThumbnail>
|
||||
<IonSkeletonText animated style={{ width: '2rem', height: '2rem' }} />
|
||||
</IonThumbnail>
|
||||
|
||||
<IonText color="dark">
|
||||
<h1 style={{ fontWeight: 'bold' }}>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</h1>
|
||||
</IonText>
|
||||
|
||||
<IonText color="medium">
|
||||
<p>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</p>
|
||||
</IonText>
|
||||
</div>
|
||||
|
||||
<IonCardTitle style={{ fontSize: '3rem' }} className="ion-margin-top">
|
||||
<IonSkeletonText animated style={{ height: '3rem', width: '30%', textAlign: 'center' }} />
|
||||
</IonCardTitle>
|
||||
|
||||
<IonGrid className="ion-margin-top">
|
||||
<IonRow>
|
||||
<IonCol size="6">
|
||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
||||
<IonCol size="3">
|
||||
<img alt="wind" src="/assets/WeatherDemo/wind.png" height="32" width="32" />
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="9">
|
||||
<IonCardSubtitle>Wind</IonCardSubtitle>
|
||||
<IonNote>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</IonNote>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="6">
|
||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
||||
<IonCol size="3">
|
||||
<IonIcon icon={thermometerOutline} color="medium" style={{ fontSize: '2rem' }} />
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="9">
|
||||
<IonCardSubtitle>Feels like</IonCardSubtitle>
|
||||
<IonNote>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</IonNote>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow className="ion-margin-top">
|
||||
<IonCol size="6">
|
||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
||||
<IonCol size="3">
|
||||
<IonIcon icon={sunnyOutline} color="medium" style={{ fontSize: '2rem' }} />
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="9">
|
||||
<IonCardSubtitle>Index UV</IonCardSubtitle>
|
||||
<IonNote>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</IonNote>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="6">
|
||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
||||
<IonCol size="3">
|
||||
<IonIcon icon={pulseOutline} color="medium" style={{ fontSize: '2rem' }} />
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="9">
|
||||
<IonCardSubtitle>Pressure</IonCardSubtitle>
|
||||
<IonNote>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</IonNote>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
</IonGrid>
|
||||
);
|
38
03_source/mobile/src/pages/DemoDictionaryApp/index.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
|
||||
|
||||
import { cloudOutline, searchOutline } from 'ionicons/icons';
|
||||
import { Route, Redirect } from 'react-router';
|
||||
|
||||
import Tab1 from './AppPages/Tab1';
|
||||
import Tab2 from './AppPages/Tab2';
|
||||
|
||||
function DemoWeatherApp() {
|
||||
return (
|
||||
<IonTabs>
|
||||
<IonRouterOutlet>
|
||||
<Route exact path="/demo-weather-app/tab1">
|
||||
<Tab1 />
|
||||
</Route>
|
||||
<Route exact path="/demo-weather-app/tab2">
|
||||
<Tab2 />
|
||||
</Route>
|
||||
<Route exact path="/demo-weather-app">
|
||||
<Redirect to="/demo-weather-app/tab1" />
|
||||
</Route>
|
||||
</IonRouterOutlet>
|
||||
{/* */}
|
||||
<IonTabBar slot="bottom">
|
||||
<IonTabButton tab="tab1" href="/demo-weather-app/tab1">
|
||||
<IonIcon icon={cloudOutline} />
|
||||
<IonLabel>Dashboard</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="tab2" href="/demo-weather-app/tab2">
|
||||
<IonIcon icon={searchOutline} />
|
||||
<IonLabel>Search</IonLabel>
|
||||
</IonTabButton>
|
||||
</IonTabBar>
|
||||
</IonTabs>
|
||||
);
|
||||
}
|
||||
|
||||
export default DemoWeatherApp;
|
103
03_source/mobile/src/pages/DemoDictionaryApp/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/WeatherDemo/img/about/madison.jpg');
|
||||
}
|
||||
|
||||
.about-header .austin {
|
||||
background-image: url('/assets/WeatherDemo/img/about/austin.jpg');
|
||||
}
|
||||
|
||||
.about-header .chicago {
|
||||
background-image: url('/assets/WeatherDemo/img/about/chicago.jpg');
|
||||
}
|
||||
|
||||
.about-header .seattle {
|
||||
background-image: url('/assets/WeatherDemo/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;
|
||||
}
|
@@ -46,6 +46,9 @@ import { Event } from './types';
|
||||
import {
|
||||
alertCircleOutline,
|
||||
alertOutline,
|
||||
apps,
|
||||
appsOutline,
|
||||
car,
|
||||
cart,
|
||||
chatbubbleOutline,
|
||||
chevronBackOutline,
|
||||
@@ -194,6 +197,61 @@ const SettingsPage: React.FC<SettingsProps> = ({
|
||||
<IonIcon icon={chevronForwardOutline}></IonIcon>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
<IonList inset={false}>
|
||||
<IonItem
|
||||
button={true}
|
||||
onClick={() => {
|
||||
router.push(paths.DEMO_CLUB_HOUSE, 'forward');
|
||||
}}
|
||||
>
|
||||
<IonIcon slot="start" icon={cart} size="large"></IonIcon>
|
||||
<IonLabel>Demo Club house</IonLabel>
|
||||
<IonIcon icon={chevronForwardOutline}></IonIcon>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
<IonList inset={false}>
|
||||
<IonItem
|
||||
button={true}
|
||||
onClick={() => {
|
||||
router.push(paths.DEMO_SCORE_BOARD, 'forward');
|
||||
}}
|
||||
>
|
||||
<IonIcon slot="start" icon={car} size="large"></IonIcon>
|
||||
<IonLabel>
|
||||
Demo Score Board <br />
|
||||
(IonCard problem)
|
||||
</IonLabel>
|
||||
<IonIcon icon={chevronForwardOutline}></IonIcon>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
{/* */}
|
||||
|
||||
<IonList inset={false}>
|
||||
<IonItem button={true} onClick={() => router.push(paths.DEMO_QUOTE_APP, 'forward')}>
|
||||
<IonIcon slot="start" icon={car} size="large"></IonIcon>
|
||||
<IonLabel>Demo Quote App</IonLabel>
|
||||
<IonIcon icon={chevronForwardOutline}></IonIcon>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
<IonList inset={false}>
|
||||
<IonItem button={true} onClick={() => router.push(paths.DEMO_QR_SCANNER, 'forward')}>
|
||||
<IonIcon slot="start" icon={car} size="large"></IonIcon>
|
||||
<IonLabel>Demo Qr scanner</IonLabel>
|
||||
<IonIcon icon={chevronForwardOutline}></IonIcon>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
<IonList inset={false}>
|
||||
<IonItem button={true} onClick={() => router.push(paths.DEMO_SHOP_APP_UI, 'forward')}>
|
||||
<IonIcon slot="start" icon={cart} size="large"></IonIcon>
|
||||
<IonLabel>Demo Shop App UI</IonLabel>
|
||||
<IonIcon icon={chevronForwardOutline}></IonIcon>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
</IonContent>
|
||||
|
||||
{/* REQ0058/logout */}
|
||||
|
144
03_source/mobile/src/pages/DemoQrScanner/AppPages/Tab1.jsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import {
|
||||
getPlatforms,
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonGrid,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonModal,
|
||||
useIonRouter,
|
||||
} from '@ionic/react';
|
||||
|
||||
import { Geolocation } from '@capacitor/geolocation';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { SkeletonDashboard } from '../components/SkeletonDashboard';
|
||||
import { chevronBackOutline, refreshOutline } from 'ionicons/icons';
|
||||
import { CurrentWeather } from '../components/CurrentWeather';
|
||||
import { useStoreState } from 'pullstate';
|
||||
import { QRStore } from '../store';
|
||||
import { getCodes } from '../store/Selectors';
|
||||
import useSound from 'use-sound';
|
||||
import openSound from '../sounds/open.wav';
|
||||
import { QRCodeScannedModal } from '../components/QRCodeScannedModal';
|
||||
import { QRWebModal } from '../components/QRWebModal';
|
||||
import { NoQRCodes } from '../components/NoQRCodes';
|
||||
import { CustomFab } from '../components/CustomFab.jsx';
|
||||
|
||||
function Tab1() {
|
||||
const pageRef = useRef(null);
|
||||
const codes = useStoreState(QRStore, getCodes);
|
||||
const [play] = useSound(openSound);
|
||||
|
||||
const [QRData, setQRData] = useState(false);
|
||||
|
||||
const handleScan = (data) => {
|
||||
if (data) {
|
||||
setQRData(data);
|
||||
play();
|
||||
handleSuccess(data);
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = (err) => {
|
||||
console.error(err);
|
||||
};
|
||||
|
||||
const start = async () => {
|
||||
const platforms = getPlatforms();
|
||||
const isWeb =
|
||||
platforms.includes('desktop') || platforms.includes('mobileweb') || platforms.includes('pwa');
|
||||
|
||||
if (!isWeb) {
|
||||
// const data = await BarcodeScanner.scan();
|
||||
|
||||
// if (data) {
|
||||
// handleSuccess(data);
|
||||
// }
|
||||
const result = await CapacitorBarcodeScanner.scanBarcode({
|
||||
hint: CapacitorBarcodeScannerTypeHint.ALL,
|
||||
scanInstructions: 'Please scan a barcode',
|
||||
scanButton: true,
|
||||
scanText: 'Scan',
|
||||
cameraDirection: CapacitorBarcodeScannerCameraDirection.BACK,
|
||||
scanOrientation: CapacitorBarcodeScannerScanOrientation.ADAPTIVE,
|
||||
android: {
|
||||
scanningLibrary: CapacitorBarcodeScannerAndroidScanningLibrary.ZXING,
|
||||
},
|
||||
});
|
||||
handleSuccess(result.ScanResult);
|
||||
} else {
|
||||
presentWebModal({
|
||||
presentingElement: pageRef.current,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSuccess = (data) => {
|
||||
setQRData(data);
|
||||
console.log(data);
|
||||
dismissWebModal();
|
||||
|
||||
play();
|
||||
present({
|
||||
presentingElement: pageRef.current,
|
||||
});
|
||||
};
|
||||
|
||||
const [present, dismiss] = useIonModal(QRCodeScannedModal, {
|
||||
dismiss: () => dismiss(),
|
||||
code: QRData,
|
||||
set: () => setQRData(),
|
||||
scan: () => start(),
|
||||
});
|
||||
|
||||
const [presentWebModal, dismissWebModal] = useIonModal(QRWebModal, {
|
||||
dismiss: () => dismissWebModal(),
|
||||
set: () => setQRData(),
|
||||
scan: handleScan,
|
||||
error: handleError,
|
||||
});
|
||||
|
||||
const router = useIonRouter();
|
||||
function handleBackButtonClick() {
|
||||
router.goBack();
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>QR Codes</IonTitle>
|
||||
|
||||
<IonButton
|
||||
slot="start"
|
||||
fill="clear"
|
||||
shape="round"
|
||||
onClick={() => handleBackButtonClick()}
|
||||
>
|
||||
<IonIcon slot="icon-only" icon={chevronBackOutline}></IonIcon>
|
||||
</IonButton>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">QR Codes</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonGrid>
|
||||
{codes.length < 1 && <NoQRCodes />}
|
||||
{codes.length > 0 && <QRCodeList codes={codes} pageRef={pageRef} />}
|
||||
</IonGrid>
|
||||
<CustomFab start={start} />
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
|
||||
export default Tab1;
|
81
03_source/mobile/src/pages/DemoQrScanner/AppPages/Tab2.jsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonSearchbar,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
import { useState } from 'react';
|
||||
import { CurrentWeather } from '../components/CurrentWeather';
|
||||
|
||||
function Tab2() {
|
||||
const [search, setSearch] = useState('');
|
||||
const [currentWeather, setCurrentWeather] = useState(false);
|
||||
|
||||
const performSearch = async () => {
|
||||
getAddress(search);
|
||||
};
|
||||
|
||||
const getAddress = async (city) => {
|
||||
const response = await fetch(
|
||||
`https://api.weatherapi.com/v1/current.json?key=f93eb660b2424258bf5155016210712&q=${city}&aqi=no`
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (data && data.current && data.location) {
|
||||
setCurrentWeather(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Search</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Search</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonRow className="ion-justify-content-center ion-margin-top ion-align-items-center">
|
||||
<IonCol size="7">
|
||||
<IonSearchbar
|
||||
placeholder="Try 'London'"
|
||||
animated
|
||||
value={search}
|
||||
onIonChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="5">
|
||||
<IonButton
|
||||
expand="block"
|
||||
className="ion-margin-start ion-margin-end"
|
||||
onClick={performSearch}
|
||||
>
|
||||
Search
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<div style={{ marginTop: '-0.8rem' }}>
|
||||
{currentWeather ? (
|
||||
<CurrentWeather currentWeather={currentWeather} />
|
||||
) : (
|
||||
<h3 className="ion-text-center">Your search result will appear here</h3>
|
||||
)}
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
|
||||
export default Tab2;
|
@@ -0,0 +1,62 @@
|
||||
import { IonCardSubtitle, IonCol, IonIcon, IonNote, IonRow } from '@ionic/react';
|
||||
import { pulseOutline, sunnyOutline, thermometerOutline } from 'ionicons/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const WeatherProperty = ({ type, currentWeather }: { type: any; currentWeather: any }) => {
|
||||
const [property, setProperty] = useState(false);
|
||||
|
||||
const properties = {
|
||||
wind: {
|
||||
isIcon: false,
|
||||
icon: '/assets/WeatherDemo/wind.png',
|
||||
alt: 'wind',
|
||||
label: 'Wind',
|
||||
value: `${currentWeather.current.wind_mph}mph`,
|
||||
},
|
||||
feelsLike: {
|
||||
isIcon: true,
|
||||
icon: thermometerOutline,
|
||||
alt: 'feels like',
|
||||
label: 'Feels like',
|
||||
value: `${currentWeather.current.feelslike_c}°C`,
|
||||
},
|
||||
indexUV: {
|
||||
isIcon: true,
|
||||
icon: sunnyOutline,
|
||||
alt: 'index uv',
|
||||
label: 'Index UV',
|
||||
value: currentWeather.current.uv,
|
||||
},
|
||||
pressure: {
|
||||
isIcon: true,
|
||||
icon: pulseOutline,
|
||||
alt: 'pressure',
|
||||
label: 'Pressure',
|
||||
value: `${currentWeather.current.pressure_mb} mbar`,
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setProperty(properties[type]);
|
||||
}, [type]);
|
||||
|
||||
return (
|
||||
<IonCol size="6">
|
||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
||||
<IonCol size="3">
|
||||
{!property.isIcon && (
|
||||
<img alt={property.alt} src={property.icon} height="32" width="32" />
|
||||
)}
|
||||
{property.isIcon && (
|
||||
<IonIcon icon={property.icon} color="medium" style={{ fontSize: '2rem' }} />
|
||||
)}
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="9">
|
||||
<IonCardSubtitle>{property.label}</IonCardSubtitle>
|
||||
<IonNote>{property.value}</IonNote>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCol>
|
||||
);
|
||||
};
|
@@ -0,0 +1,48 @@
|
||||
import { IonCard, IonCardContent, IonGrid, IonRow, IonText, IonCardTitle } from '@ionic/react';
|
||||
import { WeatherProperty } from './WeatherProperty';
|
||||
|
||||
export const CurrentWeather = ({ currentWeather }: { currentWeather: any }) => (
|
||||
<IonGrid>
|
||||
<IonCard>
|
||||
<IonCardContent className="ion-text-center">
|
||||
<IonText color="primary">
|
||||
<h1>
|
||||
{currentWeather.location.region},{' '}
|
||||
<span style={{ color: 'gray' }}>{currentWeather.location.country}</span>
|
||||
</h1>
|
||||
</IonText>
|
||||
|
||||
<div className="ion-margin-top">
|
||||
<img
|
||||
alt="condition"
|
||||
src={currentWeather.current.condition.icon.replace('//', 'https://')}
|
||||
/>
|
||||
|
||||
<IonText color="dark">
|
||||
<h1 style={{ fontWeight: 'bold' }}>{currentWeather.current.condition.text}</h1>
|
||||
</IonText>
|
||||
|
||||
<IonText color="medium">
|
||||
<p>{new Date(currentWeather.location.localtime).toDateString()}</p>
|
||||
</IonText>
|
||||
</div>
|
||||
|
||||
<IonCardTitle style={{ fontSize: '3rem' }} className="ion-margin-top">
|
||||
{currentWeather.current.temp_c}℃
|
||||
</IonCardTitle>
|
||||
|
||||
<IonGrid className="ion-margin-top">
|
||||
<IonRow>
|
||||
<WeatherProperty type="wind" currentWeather={currentWeather} />
|
||||
<WeatherProperty type="feelsLike" currentWeather={currentWeather} />
|
||||
</IonRow>
|
||||
|
||||
<IonRow className="ion-margin-top">
|
||||
<WeatherProperty type="indexUV" currentWeather={currentWeather} />
|
||||
<WeatherProperty type="pressure" currentWeather={currentWeather} />
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
</IonGrid>
|
||||
);
|
@@ -0,0 +1,27 @@
|
||||
import { IonFab, IonFabButton, IonFabList, IonIcon } from '@ionic/react';
|
||||
import { addOutline, cameraOutline, qrCodeOutline } from 'ionicons/icons';
|
||||
|
||||
export const CustomFab = ({ start }) => {
|
||||
return (
|
||||
<IonFab
|
||||
vertical="bottom"
|
||||
horizontal="end"
|
||||
slot="fixed"
|
||||
className="ion-padding-bottom ion-padding-end"
|
||||
>
|
||||
<IonFabButton>
|
||||
<IonIcon icon={qrCodeOutline} />
|
||||
</IonFabButton>
|
||||
|
||||
<IonFabList side="top" className="ion-padding-bottom">
|
||||
<IonFabButton color="primary" onClick={start}>
|
||||
<IonIcon icon={cameraOutline} />
|
||||
</IonFabButton>
|
||||
|
||||
<IonFabButton color="primary" routerLink="/demo-qr-scanner/manual">
|
||||
<IonIcon icon={addOutline} />
|
||||
</IonFabButton>
|
||||
</IonFabList>
|
||||
</IonFab>
|
||||
);
|
||||
};
|
@@ -0,0 +1,15 @@
|
||||
import { IonCol, IonRow, IonText } from '@ionic/react';
|
||||
|
||||
export const NoQRCodes = () => (
|
||||
<IonRow className="ion-text-center ion-justify-content-center">
|
||||
<IonCol size="9">
|
||||
<h3>It looks like you don't have any QR codes stored.</h3>
|
||||
<img src="/assets/icon2.png" alt="icon" />
|
||||
|
||||
<p>
|
||||
Click the <IonText color="primary">button</IonText> in the bottom right to scan a code or
|
||||
generate a code.
|
||||
</p>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
);
|
@@ -0,0 +1,106 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonCard,
|
||||
IonCardContent,
|
||||
IonCardHeader,
|
||||
IonCardTitle,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonGrid,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonNote,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonToast,
|
||||
} from '@ionic/react';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { addQRCode } from '../store/QRStore';
|
||||
|
||||
import useSound from 'use-sound';
|
||||
import closeSound from '../sounds/close.wav';
|
||||
import { reloadOutline } from 'ionicons/icons';
|
||||
|
||||
export const QRCodeScannedModal = ({ dismiss, code, set, scan }) => {
|
||||
const [play] = useSound(closeSound);
|
||||
const [showToast] = useIonToast();
|
||||
|
||||
const handleDismiss = () => {
|
||||
dismiss();
|
||||
play();
|
||||
};
|
||||
|
||||
const handleScanAgain = () => {
|
||||
handleDismiss();
|
||||
|
||||
setTimeout(() => {
|
||||
scan();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
const handleAdd = async () => {
|
||||
addQRCode(code.text ? code.text : code, true);
|
||||
showToast({
|
||||
header: 'Success!',
|
||||
message: 'QR Code stored successfully.',
|
||||
duration: 3000,
|
||||
color: 'primary',
|
||||
});
|
||||
|
||||
handleDismiss();
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>View QR Code</IonTitle>
|
||||
<IonButtons slot="end">
|
||||
<IonButton onClick={handleDismiss}>Close</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent>
|
||||
<IonGrid className="ion-padding-top ion-margin-top">
|
||||
<IonRow className="ion-justify-content-center ion-text-center animate__animated animate__lightSpeedInLeft animate__faster">
|
||||
<IonCol size="12">
|
||||
<QRCode value={code.text ? code.text : code} />
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<IonCard>
|
||||
<IonCardHeader>
|
||||
<IonCardTitle>QR Code data</IonCardTitle>
|
||||
<IonNote>This is what the code represents</IonNote>
|
||||
</IonCardHeader>
|
||||
<IonCardContent>
|
||||
<p>{code.text ? code.text : code}</p>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="6">
|
||||
<IonButton expand="block" fill="outline" onClick={handleScanAgain}>
|
||||
<IonIcon icon={reloadOutline} />
|
||||
Scan again
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
<IonCol size="6">
|
||||
<IonButton expand="block" onClick={handleAdd}>
|
||||
Store →
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonGrid,
|
||||
IonHeader,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
// import QrReader from "react-qr-reader";
|
||||
|
||||
export const QRWebModal = ({ dismiss, set, scan, error }) => {
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Scan QR Code</IonTitle>
|
||||
<IonButtons slot="end">
|
||||
<IonButton onClick={dismiss}>Close</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent>
|
||||
<IonGrid className="ion-padding-top ion-margin-top">
|
||||
<IonRow className="ion-justify-content-center ion-text-center animate__animated animate__lightSpeedInLeft animate__faster">
|
||||
<IonCol size="12">
|
||||
{/*
|
||||
<QrReader
|
||||
delay={ 500 }
|
||||
onError={ error }
|
||||
onScan={ scan }
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
/>
|
||||
*/}
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
IonCard,
|
||||
IonCardContent,
|
||||
IonCardSubtitle,
|
||||
IonCardTitle,
|
||||
IonCol,
|
||||
IonGrid,
|
||||
IonIcon,
|
||||
IonNote,
|
||||
IonRow,
|
||||
IonSkeletonText,
|
||||
IonText,
|
||||
IonThumbnail,
|
||||
} from '@ionic/react';
|
||||
import { pulseOutline, sunnyOutline, thermometerOutline } from 'ionicons/icons';
|
||||
|
||||
export const SkeletonDashboard = () => (
|
||||
<IonGrid>
|
||||
<IonCard>
|
||||
<IonCardContent className="ion-text-center">
|
||||
<IonText color="primary">
|
||||
<h1>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</h1>
|
||||
</IonText>
|
||||
|
||||
<div className="ion-margin-top">
|
||||
<IonThumbnail>
|
||||
<IonSkeletonText animated style={{ width: '2rem', height: '2rem' }} />
|
||||
</IonThumbnail>
|
||||
|
||||
<IonText color="dark">
|
||||
<h1 style={{ fontWeight: 'bold' }}>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</h1>
|
||||
</IonText>
|
||||
|
||||
<IonText color="medium">
|
||||
<p>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</p>
|
||||
</IonText>
|
||||
</div>
|
||||
|
||||
<IonCardTitle style={{ fontSize: '3rem' }} className="ion-margin-top">
|
||||
<IonSkeletonText animated style={{ height: '3rem', width: '30%', textAlign: 'center' }} />
|
||||
</IonCardTitle>
|
||||
|
||||
<IonGrid className="ion-margin-top">
|
||||
<IonRow>
|
||||
<IonCol size="6">
|
||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
||||
<IonCol size="3">
|
||||
<img alt="wind" src="/assets/WeatherDemo/wind.png" height="32" width="32" />
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="9">
|
||||
<IonCardSubtitle>Wind</IonCardSubtitle>
|
||||
<IonNote>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</IonNote>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="6">
|
||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
||||
<IonCol size="3">
|
||||
<IonIcon icon={thermometerOutline} color="medium" style={{ fontSize: '2rem' }} />
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="9">
|
||||
<IonCardSubtitle>Feels like</IonCardSubtitle>
|
||||
<IonNote>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</IonNote>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow className="ion-margin-top">
|
||||
<IonCol size="6">
|
||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
||||
<IonCol size="3">
|
||||
<IonIcon icon={sunnyOutline} color="medium" style={{ fontSize: '2rem' }} />
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="9">
|
||||
<IonCardSubtitle>Index UV</IonCardSubtitle>
|
||||
<IonNote>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</IonNote>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="6">
|
||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
||||
<IonCol size="3">
|
||||
<IonIcon icon={pulseOutline} color="medium" style={{ fontSize: '2rem' }} />
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="9">
|
||||
<IonCardSubtitle>Pressure</IonCardSubtitle>
|
||||
<IonNote>
|
||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
||||
</IonNote>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
</IonGrid>
|
||||
);
|
38
03_source/mobile/src/pages/DemoQrScanner/index.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
|
||||
|
||||
import { cloudOutline, searchOutline } from 'ionicons/icons';
|
||||
import { Route, Redirect } from 'react-router';
|
||||
|
||||
import Tab1 from './AppPages/Tab1';
|
||||
import Tab2 from './AppPages/Tab2';
|
||||
|
||||
function DemoQrScanner() {
|
||||
return (
|
||||
<IonTabs>
|
||||
<IonRouterOutlet>
|
||||
<Route exact={true} path="/demo-qr-scanner/home">
|
||||
<Tab1 />
|
||||
</Route>
|
||||
|
||||
<Route exact={true} path="/demo-qr-scanner/manual">
|
||||
<Tab2 />
|
||||
</Route>
|
||||
|
||||
<Redirect exact={true} path="/demo-qr-scanner" to="/demo-qr-scanner/home" />
|
||||
</IonRouterOutlet>
|
||||
{/* */}
|
||||
<IonTabBar slot="bottom">
|
||||
<IonTabButton tab="home" href="/demo-qr-scanner/home">
|
||||
<IonIcon icon={cloudOutline} />
|
||||
<IonLabel>Dashboard</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="tab2" href="/demo-qr-scanner/tab2">
|
||||
<IonIcon icon={searchOutline} />
|
||||
<IonLabel>Search</IonLabel>
|
||||
</IonTabButton>
|
||||
</IonTabBar>
|
||||
</IonTabs>
|
||||
);
|
||||
}
|
||||
|
||||
export default DemoQrScanner;
|
BIN
03_source/mobile/src/pages/DemoQrScanner/sounds/close.wav
Normal file
BIN
03_source/mobile/src/pages/DemoQrScanner/sounds/open.wav
Normal file
19
03_source/mobile/src/pages/DemoQrScanner/store/QRStore.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Store } from 'pullstate';
|
||||
|
||||
const QRStore = new Store({
|
||||
codes: [],
|
||||
});
|
||||
|
||||
export default QRStore;
|
||||
|
||||
export const addQRCode = (data, scanned = false) => {
|
||||
QRStore.update((s) => {
|
||||
s.codes = [...s.codes, { id: new Date(), data, scanned }];
|
||||
});
|
||||
};
|
||||
|
||||
export const removeQRCode = (id) => {
|
||||
QRStore.update((s) => {
|
||||
s.codes = s.codes.filter((code) => code.id !== id);
|
||||
});
|
||||
};
|
@@ -0,0 +1,6 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
const getState = (state) => state;
|
||||
|
||||
// General getters
|
||||
export const getCodes = createSelector(getState, (state) => state.codes);
|
1
03_source/mobile/src/pages/DemoQrScanner/store/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default as QRStore } from './QRStore';
|
103
03_source/mobile/src/pages/DemoQrScanner/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/WeatherDemo/img/about/madison.jpg');
|
||||
}
|
||||
|
||||
.about-header .austin {
|
||||
background-image: url('/assets/WeatherDemo/img/about/austin.jpg');
|
||||
}
|
||||
|
||||
.about-header .chicago {
|
||||
background-image: url('/assets/WeatherDemo/img/about/chicago.jpg');
|
||||
}
|
||||
|
||||
.about-header .seattle {
|
||||
background-image: url('/assets/WeatherDemo/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;
|
||||
}
|
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
export { default as QuoteStore } from "./QuoteStore";
|
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;
|
||||
}
|
@@ -0,0 +1,142 @@
|
||||
import {
|
||||
IonBadge,
|
||||
IonButton,
|
||||
IonCard,
|
||||
IonCardContent,
|
||||
IonCardTitle,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonGrid,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonList,
|
||||
IonPage,
|
||||
IonRow,
|
||||
useIonModal,
|
||||
useIonRouter,
|
||||
useIonViewWillEnter,
|
||||
} from '@ionic/react';
|
||||
import { addOutline, arrowBack } from 'ionicons/icons';
|
||||
import { useStoreState } from 'pullstate';
|
||||
import { useRef } from 'react';
|
||||
import { MainStore } from '../store';
|
||||
import { getScoreboard } from '../store/Selectors';
|
||||
|
||||
import './Page.css';
|
||||
import styles from './ActiveScoreboard.module.scss';
|
||||
import { addScoreToPlayer } from '../store/MainStore';
|
||||
import FinishModal from '../components/FinishModal';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
const ActiveScoreboard = () => {
|
||||
const pageRef = useRef();
|
||||
const headingRef = useRef();
|
||||
const router = useIonRouter();
|
||||
|
||||
const { id } = useParams();
|
||||
const activeScoreboard = useStoreState(MainStore, getScoreboard(id));
|
||||
|
||||
const [present, dismiss] = useIonModal(FinishModal, {
|
||||
dismiss: () => dismiss(),
|
||||
});
|
||||
|
||||
useIonViewWillEnter(() => {
|
||||
headingRef.current.classList.add('animate__slideInDown');
|
||||
headingRef.current.style.display = '';
|
||||
});
|
||||
|
||||
const handleAddScore = (index) => {
|
||||
addScoreToPlayer(activeScoreboard.id, index);
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage ref={pageRef}>
|
||||
<IonHeader>
|
||||
<div className={styles.customHeader}>
|
||||
<img
|
||||
alt="header"
|
||||
src="/assets/ScoreBoard/scoreboardheader.jpeg"
|
||||
className="animate__animated animate__slideInRight animate__faster"
|
||||
/>
|
||||
|
||||
<div className="ion-justify-content-between">
|
||||
<div className={styles.customBackButton} onClick={() => router.goBack()}>
|
||||
<IonIcon icon={arrowBack} color="light" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`${styles.mainContent} animate__animated`}
|
||||
ref={headingRef}
|
||||
style={{ display: 'none' }}
|
||||
>
|
||||
<IonGrid>
|
||||
<IonRow>
|
||||
<IonCol size="10">
|
||||
<IonCard className={styles.placeHeading}>
|
||||
<IonCardContent>
|
||||
<IonCardTitle>{activeScoreboard.title}</IonCardTitle>
|
||||
<p>{activeScoreboard.players.length} players</p>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</div>
|
||||
</div>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
{activeScoreboard.done && (
|
||||
<IonRow>
|
||||
<IonCol size="12" className="ion-text-center">
|
||||
<h3>Game has finished.</h3>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
)}
|
||||
|
||||
<IonList>
|
||||
{activeScoreboard.players.map((player, index) => {
|
||||
return (
|
||||
<IonItem
|
||||
lines="none"
|
||||
className={`${styles.playerItem} animate__animated animate__fadeIn`}
|
||||
>
|
||||
<IonBadge color="light" className="ion-margin-end">
|
||||
{index + 1}
|
||||
</IonBadge>
|
||||
<IonLabel className="ion-text-wrap">{player.name}</IonLabel>
|
||||
<IonLabel>{player.score} points</IonLabel>
|
||||
<IonButton
|
||||
disabled={activeScoreboard.done || !activeScoreboard.active}
|
||||
color="light"
|
||||
onClick={() => handleAddScore(index)}
|
||||
>
|
||||
<IonIcon icon={addOutline} />
|
||||
</IonButton>
|
||||
</IonItem>
|
||||
);
|
||||
})}
|
||||
</IonList>
|
||||
|
||||
<IonButton
|
||||
disabled={activeScoreboard.done || !activeScoreboard.active}
|
||||
className="ion-padding-start ion-padding-end"
|
||||
expand="block"
|
||||
color="dark"
|
||||
onClick={() =>
|
||||
present({
|
||||
presentingElement: pageRef.current,
|
||||
})
|
||||
}
|
||||
>
|
||||
Finish Game →
|
||||
</IonButton>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActiveScoreboard;
|
@@ -0,0 +1,63 @@
|
||||
.customHeader {
|
||||
|
||||
img {
|
||||
|
||||
border-bottom-left-radius: 150px;
|
||||
// border-bottom-right-radius: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.customBackButton {
|
||||
|
||||
position: absolute;
|
||||
top: 3rem;
|
||||
left: 1.4rem;
|
||||
border-radius: 500px;
|
||||
padding: 0.5rem;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border: 2px solid white;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
|
||||
color: rgb(255, 255, 255);
|
||||
height: 1.75rem;
|
||||
width: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
|
||||
// position: relative;
|
||||
margin-top: -5rem !important;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.placeHeading {
|
||||
|
||||
$grad_color: #f8f8f8;
|
||||
$main_color: white;
|
||||
|
||||
background:
|
||||
linear-gradient(135deg, $grad_color 25%, transparent 25%) -50px 0,
|
||||
linear-gradient(225deg, $grad_color 25%, transparent 25%) -50px 0,
|
||||
linear-gradient(315deg, $grad_color 25%, transparent 25%),
|
||||
linear-gradient(45deg, $grad_color 25%, transparent 25%);
|
||||
background-size: 100px 100px;
|
||||
background-color: $main_color;
|
||||
}
|
||||
|
||||
.playerItem {
|
||||
|
||||
margin-left: 1.2rem;
|
||||
margin-right: 1.2rem;
|
||||
--ion-item-background: var(--ion-color-primary);
|
||||
--color: white;
|
||||
--border-radius: 10px;
|
||||
--padding-top: 0.5rem;
|
||||
--padding-bottom: 0.5rem;
|
||||
}
|
179
03_source/mobile/src/pages/DemoScoreBoard/AppPages/Dashboard.jsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonCard,
|
||||
IonCardContent,
|
||||
IonModal,
|
||||
IonCardSubtitle,
|
||||
IonCardTitle,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonList,
|
||||
IonMenuButton,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonText,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonModal,
|
||||
useIonRouter,
|
||||
IonCardHeader,
|
||||
} from '@ionic/react';
|
||||
import { arrowForward, arrowUndoOutline, chevronBack, contractOutline } from 'ionicons/icons';
|
||||
import { useStoreState } from 'pullstate';
|
||||
import { useRef, useState } from 'react';
|
||||
import GenerateModal from '../components/GenerateModal';
|
||||
import { MainStore } from '../store';
|
||||
import { getActiveScoreboard } from '../store/Selectors';
|
||||
|
||||
import './Page.css';
|
||||
|
||||
const Dashboard = () => {
|
||||
const pageRef = useRef();
|
||||
const router = useIonRouter();
|
||||
|
||||
const activeScoreboard = useStoreState(MainStore, getActiveScoreboard);
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
const [presentGenerateModal, dismissGenerateModal] = useIonModal(GenerateModal, {
|
||||
dismiss: () => dismissGenerateModal(),
|
||||
});
|
||||
|
||||
const features = [
|
||||
{
|
||||
label: 'Track scores easily',
|
||||
icon: contractOutline,
|
||||
},
|
||||
{
|
||||
label: 'See previous scoreboards',
|
||||
icon: arrowUndoOutline,
|
||||
},
|
||||
];
|
||||
|
||||
const Feature = ({ feature }) => (
|
||||
<IonItem lines="none">
|
||||
<IonIcon icon={feature.icon} />
|
||||
<IonLabel className="ion-padding-start">{feature.label}</IonLabel>
|
||||
</IonItem>
|
||||
);
|
||||
|
||||
const handleShow = () => {
|
||||
presentGenerateModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage ref={pageRef}>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonButton
|
||||
shape={'round'}
|
||||
onClick={() => {
|
||||
router.goBack();
|
||||
}}
|
||||
>
|
||||
<IonIcon icon={chevronBack}></IonIcon>
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
<IonTitle>Dashboard</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Dashboard</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonCard>helloworld</IonCard>
|
||||
|
||||
<IonCard className="animate__animated animate__slideInLeft">
|
||||
<IonCardContent>
|
||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
||||
<IonCol size="3">
|
||||
<img
|
||||
src="/assets/ScoreBoard/icons/dashboard_ionicreacthub.png"
|
||||
width="80"
|
||||
height="80"
|
||||
alt="icon"
|
||||
/>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="9">
|
||||
<IonCardTitle>Ionic Scoreboard</IonCardTitle>
|
||||
<p>Track scores easily for games!</p>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonList>
|
||||
{features.map((feature, index) => (
|
||||
<Feature key={index} feature={feature} />
|
||||
))}
|
||||
</IonList>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
{activeScoreboard && (
|
||||
<IonCard className="animate__animated animate__slideInLeft active-scoreboard-card">
|
||||
<IonCardContent>
|
||||
<IonCardTitle>Active Scoreboard</IonCardTitle>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="6">
|
||||
<IonCardSubtitle color="light">Title</IonCardSubtitle>
|
||||
<IonText color="light">
|
||||
<p className="ion-text-wrap">{activeScoreboard.title}</p>
|
||||
</IonText>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="3" className="ion-text-center">
|
||||
<IonCardSubtitle color="light">Players</IonCardSubtitle>
|
||||
<IonText color="light">
|
||||
<p>{activeScoreboard.players.length}</p>
|
||||
</IonText>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="2">
|
||||
<IonButton
|
||||
disabled={activeScoreboard.done}
|
||||
color="light"
|
||||
fill="outline"
|
||||
routerLink={`/page/active-scoreboard/${activeScoreboard.id}`}
|
||||
>
|
||||
<IonIcon icon={arrowForward} />
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
{activeScoreboard.done && (
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<IonCardSubtitle color="light">Scoreboard finished.</IonCardSubtitle>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
)}
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
)}
|
||||
<IonCard className="animate__animated animate__slideInLeft">
|
||||
<IonCardContent>
|
||||
<IonCardTitle>Ready to get started?</IonCardTitle>
|
||||
<IonButton expand="block" className="ion-margin-top" onClick={() => setShowModal(true)}>
|
||||
Generate a scoreboard →
|
||||
</IonButton>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
<IonModal isOpen={showModal}>
|
||||
<GenerateModal dismiss={() => setShowModal(false)} />
|
||||
</IonModal>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
11
03_source/mobile/src/pages/DemoScoreBoard/AppPages/Page.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.active-scoreboard-card {
|
||||
|
||||
--ion-card-background: var(--ion-color-primary);
|
||||
--ion-text-color: white;
|
||||
}
|
||||
|
||||
.player-square {
|
||||
|
||||
/* border: 1px solid red; */
|
||||
/* height: 5rem; */
|
||||
}
|
46
03_source/mobile/src/pages/DemoScoreBoard/AppPages/Page.jsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
IonButtons,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonMenuButton,
|
||||
IonPage,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import './Page.css';
|
||||
|
||||
const Page = () => {
|
||||
const { name } = useParams();
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>{name}</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">{name}</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<img
|
||||
src="/assets/ScoreBoard/icons/dashboard_ionicreacthub.png"
|
||||
width="200"
|
||||
height="200"
|
||||
alt="icon"
|
||||
/>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
@@ -0,0 +1,86 @@
|
||||
import { IonButton, IonButtons, IonCard, IonCardContent, IonCardSubtitle, IonCol, IonContent, IonHeader, IonIcon, IonLabel, IonMenuButton, IonPage, IonRow, IonText, IonTitle, IonToolbar } from '@ionic/react';
|
||||
import { arrowForward } from 'ionicons/icons';
|
||||
import { useStoreState } from 'pullstate';
|
||||
import { useRef } from 'react';
|
||||
import { MainStore } from '../store';
|
||||
import { getScoreboards } from '../store/Selectors';
|
||||
|
||||
import './Page.css';
|
||||
|
||||
const PreviousScoreboards = () => {
|
||||
|
||||
const pageRef = useRef();
|
||||
const scoreboards = useStoreState(MainStore, getScoreboards)
|
||||
|
||||
return (
|
||||
<IonPage ref={ pageRef }>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Previous Scoreboards</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Previous Scoreboards</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
{ scoreboards.length > 0 &&
|
||||
|
||||
<>
|
||||
{ scoreboards.map((scoreboard, index) => {
|
||||
|
||||
return (
|
||||
|
||||
<IonCard key={ index } className="animate__animated animate__slideInLeft active-scoreboard-card">
|
||||
<IonCardContent>
|
||||
<IonRow>
|
||||
<IonCol size="6">
|
||||
<IonCardSubtitle color="light">Title</IonCardSubtitle>
|
||||
<IonText color="light">
|
||||
<p className="ion-text-wrap">{ scoreboard.title }</p>
|
||||
</IonText>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="3" className="ion-text-center">
|
||||
<IonCardSubtitle color="light">Players</IonCardSubtitle>
|
||||
<IonText color="light">
|
||||
<p>{ scoreboard.players && scoreboard.players.length }</p>
|
||||
</IonText>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="2">
|
||||
<IonButton color="light" fill="outline" routerLink={ `/page/active-scoreboard/${ scoreboard.id }`}>
|
||||
<IonIcon icon={ arrowForward } />
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
}
|
||||
|
||||
{ scoreboards.length < 1 &&
|
||||
<IonRow>
|
||||
<IonCol size="12" className="ion-text-center">
|
||||
<IonLabel color="primary">
|
||||
<h1>No scoreboards to show</h1>
|
||||
<p>You can easily add a new one</p>
|
||||
</IonLabel>
|
||||
<IonButton className="ion-margin-top" color="primary" fill="outline" routerLink="/page/Dashboard">Add one →</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
}
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreviousScoreboards;
|
@@ -0,0 +1,24 @@
|
||||
import { IonCard, IonCardContent, IonCardTitle, IonCol, IonInput, IonItem, IonLabel, IonNote, IonRow } from "@ionic/react";
|
||||
|
||||
export const Details = ({ details, setDetails }) => (
|
||||
|
||||
<IonCard>
|
||||
<IonCardContent>
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<IonCardTitle>Details</IonCardTitle>
|
||||
<IonNote>Some basic info</IonNote>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<IonItem lines="none" className="ion-no-padding">
|
||||
<IonLabel position="stacked">Scoreboard title</IonLabel>
|
||||
<IonInput value={ details.title } onIonChange={ e => setDetails({ ...details, title: e.target.value })} placeholder="Give your scoreboard a title" />
|
||||
</IonItem>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
);
|
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonCard,
|
||||
IonCardContent,
|
||||
IonCardTitle,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonGrid,
|
||||
IonHeader,
|
||||
IonNote,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
import { useStoreState } from 'pullstate';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { MainStore } from '../store';
|
||||
import { getActiveScoreboard } from '../store/Selectors';
|
||||
|
||||
import Confetti from 'react-confetti';
|
||||
import { useWindowSize } from '@react-hook/window-size';
|
||||
import { markActiveAsDone } from '../store/MainStore';
|
||||
|
||||
const FinishModal = ({ dismiss }) => {
|
||||
const activeScoreboard = useStoreState(MainStore, getActiveScoreboard);
|
||||
const [winner, setWinner] = useState({});
|
||||
const [width, height] = useWindowSize();
|
||||
|
||||
useEffect(() => {
|
||||
activeScoreboard && setWinner(activeScoreboard.players[0]);
|
||||
}, [activeScoreboard]);
|
||||
|
||||
const finish = () => {
|
||||
markActiveAsDone();
|
||||
dismiss();
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<Confetti width={width} height={height} />
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Results!</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonGrid>
|
||||
<IonRow className="animate__animated animate__slideInLeft">
|
||||
<IonCol size="12">
|
||||
<IonCard>
|
||||
<IonCardContent>
|
||||
<IonRow>
|
||||
<IonCol size="12" className="ion-text-center">
|
||||
<img
|
||||
alt="winner"
|
||||
height="150"
|
||||
width="150"
|
||||
src="/assets/ScoreBoard/icons/crown_ionicreacthub.png"
|
||||
/>
|
||||
<IonCardTitle>Winner</IonCardTitle>
|
||||
<IonNote>with {winner.score} points</IonNote>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="12" className="ion-text-center">
|
||||
<h1>{winner.name}</h1>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
|
||||
<IonButton
|
||||
color="primary"
|
||||
expand="block"
|
||||
className="ion-padding-start ion-padding-end"
|
||||
onClick={finish}
|
||||
>
|
||||
Done
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default FinishModal;
|
@@ -0,0 +1,66 @@
|
||||
import { IonButton, IonButtons, IonCol, IonContent, IonGrid, IonHeader, IonPage, IonRow, IonTitle, IonToolbar, useIonToast } from '@ionic/react';
|
||||
import { useState } from 'react';
|
||||
import { addScoreboard } from '../store/MainStore';
|
||||
import { Details } from './Details';
|
||||
import { Players } from './Players';
|
||||
|
||||
const GenerateModal = ({ dismiss }) => {
|
||||
|
||||
const [ players, setPlayers ] = useState([]);
|
||||
const [ details, setDetails ] = useState({});
|
||||
const [ showToast ] = useIonToast();
|
||||
|
||||
const save = () => {
|
||||
|
||||
addScoreboard(players, details);
|
||||
showToast({
|
||||
|
||||
header: "Success",
|
||||
message: "Scoreboard added successfully.",
|
||||
color: "primary",
|
||||
duration: 3500
|
||||
});
|
||||
|
||||
dismiss();
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonButton onClick={ dismiss }>Close</IonButton>
|
||||
</IonButtons>
|
||||
<IonTitle>Generate a scoreboard</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
|
||||
<IonGrid>
|
||||
|
||||
<IonRow className="animate__animated animate__slideInLeft">
|
||||
<IonCol size="12">
|
||||
<Details details={ details } setDetails={ setDetails } />
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow className="animate__animated animate__slideInLeft">
|
||||
<IonCol size="12">
|
||||
<Players players={ players } setPlayers={ setPlayers } />
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow className="animate__animated animate__slideInLeft">
|
||||
<IonCol size="12">
|
||||
<IonButton expand="block" color="primary" onClick={ save }>Save</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenerateModal;
|
113
03_source/mobile/src/pages/DemoScoreBoard/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,46 @@
|
||||
import { IonContent, IonIcon, IonItem, IonLabel, IonList, IonListHeader, IonMenu, IonMenuToggle, IonNote } from '@ionic/react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { arrowUndoOutline, arrowUndoSharp, pieChartOutline, pieChartSharp } from 'ionicons/icons';
|
||||
import './Menu.css';
|
||||
|
||||
const appPages = [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
url: '/page/Dashboard',
|
||||
iosIcon: pieChartOutline,
|
||||
mdIcon: pieChartSharp
|
||||
},
|
||||
{
|
||||
title: 'Previous scoreboards',
|
||||
url: '/page/Previous',
|
||||
iosIcon: arrowUndoOutline,
|
||||
mdIcon: arrowUndoSharp
|
||||
}
|
||||
];
|
||||
|
||||
const Menu = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<IonMenu contentId="main" type="overlay">
|
||||
<IonContent>
|
||||
<IonList id="inbox-list">
|
||||
<IonListHeader>Ionic Scoreboard</IonListHeader>
|
||||
<IonNote>An awesome scoreboard</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,26 @@
|
||||
import { IonCheckbox, IonCol, IonIcon, IonInput, IonItem, IonItemOption, IonItemOptions, IonItemSliding, IonLabel, IonRow } from "@ionic/react";
|
||||
import { trashOutline } from "ionicons/icons";
|
||||
|
||||
export const Player = ({ player, index, handleChange, handleRemove }) => {
|
||||
|
||||
return (
|
||||
|
||||
<IonItemSliding>
|
||||
<IonItem className="ion-no-padding animate__animated animate__fadeIn" lines="full">
|
||||
<IonRow>
|
||||
|
||||
<IonCol size="12">
|
||||
<IonLabel position="stacked">Name</IonLabel>
|
||||
<IonInput value={ player.name } onIonChange={ e => handleChange(e, index, "name") } placeholder="Enter a name" type="text" enterkeyhint="done" />
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonItem>
|
||||
|
||||
<IonItemOptions side="end">
|
||||
<IonItemOption color="danger" style={{ paddingLeft: "1rem", paddingRight: "1rem" }} onClick={ () => handleRemove(index) }>
|
||||
<IonIcon icon={ trashOutline } />
|
||||
</IonItemOption>
|
||||
</IonItemOptions>
|
||||
</IonItemSliding>
|
||||
);
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
import { IonButton, IonCard, IonCardContent, IonCardTitle, IonCol, IonList, IonNote, IonRow } from "@ionic/react";
|
||||
import { Player } from "./Player";
|
||||
|
||||
export const Players = ({ players, setPlayers }) => {
|
||||
|
||||
const handlePlayerChange = (e, index, field) => {
|
||||
|
||||
const newValue = e.target.value;
|
||||
const newPlayers = [ ...players ];
|
||||
|
||||
newPlayers[index][field] = newValue;
|
||||
setPlayers(newPlayers);
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
|
||||
const tempPlayer = {
|
||||
|
||||
name: "",
|
||||
score: 0
|
||||
};
|
||||
|
||||
setPlayers([ ...players, tempPlayer ]);
|
||||
}
|
||||
|
||||
const handleRemove = index => {
|
||||
|
||||
setPlayers(current => current.filter((c, i) => i !== index));
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<IonCard>
|
||||
<IonCardContent>
|
||||
<IonRow>
|
||||
<IonCol size="9">
|
||||
<IonCardTitle>Players</IonCardTitle>
|
||||
<IonNote>Add some players</IonNote>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="3">
|
||||
<IonButton color="primary" onClick={ handleAdd }>Add</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
{ players.length > 0 &&
|
||||
|
||||
<IonList className="ion-margin-top ion-padding-top">
|
||||
{ players.map((player, index) => <Player key={ index } player={ player } index={ index } handleChange={ handlePlayerChange } handleRemove={ handleRemove } /> )}
|
||||
</IonList>
|
||||
}
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
);
|
||||
}
|
66
03_source/mobile/src/pages/DemoScoreBoard/index.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonLabel,
|
||||
IonPage,
|
||||
IonRouterOutlet,
|
||||
IonRow,
|
||||
IonTabBar,
|
||||
IonTabButton,
|
||||
IonTabs,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonRouter,
|
||||
} from '@ionic/react';
|
||||
import { IonApp, IonSplitPane } from '@ionic/react';
|
||||
|
||||
import { Geolocation } from '@capacitor/geolocation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SkeletonDashboard } from './components1/SkeletonDashboard';
|
||||
import {
|
||||
addCircle,
|
||||
addCircleOutline,
|
||||
chevronBack,
|
||||
cloudOutline,
|
||||
home,
|
||||
homeOutline,
|
||||
notifications,
|
||||
notificationsOutline,
|
||||
person,
|
||||
personOutline,
|
||||
refreshOutline,
|
||||
search,
|
||||
searchOutline,
|
||||
} from 'ionicons/icons';
|
||||
import { IonReactRouter } from '@ionic/react-router';
|
||||
import { Route, Redirect } from 'react-router';
|
||||
//
|
||||
import Dashboard from './AppPages/Dashboard';
|
||||
import PreviousScoreboards from './AppPages/PreviousScoreboards';
|
||||
import ActiveScoreboard from './AppPages/ActiveScoreboard';
|
||||
|
||||
const DemoScoreBoard = () => {
|
||||
return (
|
||||
<>
|
||||
<Route path="/demo-score-board/Dashboard" exact={true}>
|
||||
<Dashboard />
|
||||
</Route>
|
||||
|
||||
<Route path="/demo-score-board/Previous" exact={true}>
|
||||
<PreviousScoreboards />
|
||||
</Route>
|
||||
|
||||
<Route path="/demo-score-board/active-scoreboard/:id" exact={true}>
|
||||
<ActiveScoreboard />
|
||||
</Route>
|
||||
|
||||
<Redirect path="/demo-score-board" exact={true} to="/demo-score-board/Dashboard" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DemoScoreBoard;
|
59
03_source/mobile/src/pages/DemoScoreBoard/store/MainStore.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Store } from "pullstate";
|
||||
|
||||
const MainStore = new Store({
|
||||
|
||||
players: [],
|
||||
scoreboards: []
|
||||
});
|
||||
|
||||
export default MainStore;
|
||||
|
||||
export const markActiveAsDone = () => {
|
||||
|
||||
MainStore.update(state => {
|
||||
|
||||
const scoreboardIndex = state.scoreboards.findIndex(scoreboard => scoreboard.active === true);
|
||||
state.scoreboards[scoreboardIndex].done = true;
|
||||
});
|
||||
}
|
||||
|
||||
export const addScoreboard = (players, details) => {
|
||||
|
||||
MainStore.update(s => { s.scoreboards = s.scoreboards.map(scoreboard => scoreboard.active = false) });
|
||||
|
||||
MainStore.update(state => {
|
||||
|
||||
state.scoreboards.forEach((scoreboard, index) => {
|
||||
|
||||
state.scoreboards[index].active = false;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
const newScoreboard = {
|
||||
|
||||
id: Date.now(),
|
||||
title: details.title,
|
||||
players: [ ...players ],
|
||||
active: true
|
||||
};
|
||||
|
||||
const playersToSave = players.filter(p => p.saved === true);
|
||||
|
||||
MainStore.update(s => { s.scoreboards = [ ...s.scoreboards, newScoreboard ] });
|
||||
MainStore.update(s => { s.players = [ ...s.players, ...playersToSave ] });
|
||||
}
|
||||
|
||||
export const addScoreToPlayer = (scoreboardId, playerIndex) => {
|
||||
|
||||
MainStore.update(state => {
|
||||
|
||||
const scoreboardIndex = state.scoreboards.findIndex(scoreboard => scoreboard.id === parseInt(scoreboardId));
|
||||
state.scoreboards[scoreboardIndex].players[playerIndex].score += 1;
|
||||
|
||||
state.scoreboards[scoreboardIndex].players.sort((a, b) => {
|
||||
if (a.score > b.score) return -1
|
||||
return a.score < b.score ? 1 : 0
|
||||
});
|
||||
});
|
||||
}
|
10
03_source/mobile/src/pages/DemoScoreBoard/store/Selectors.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
const getState = state => state;
|
||||
|
||||
// General getters
|
||||
export const getScoreboards = createSelector(getState, state => state.scoreboards);
|
||||
|
||||
// Specific getters
|
||||
export const getActiveScoreboard = createSelector(getState, state => state.scoreboards.filter(scoreboard => scoreboard.active === true)[0]);
|
||||
export const getScoreboard = id => createSelector(getState, state => state.scoreboards.filter(scoreboard => parseInt(scoreboard.id) === parseInt(id))[0]);
|
1
03_source/mobile/src/pages/DemoScoreBoard/store/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default as MainStore } from "./MainStore";
|
103
03_source/mobile/src/pages/DemoScoreBoard/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;
|
||||
}
|
97
03_source/mobile/src/pages/DemoShopAppUi/AppPages/Cart.jsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonGrid,
|
||||
IonHeader,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
import { useStoreState } from 'pullstate';
|
||||
import { useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { CartProduct } from '../components/CartProduct';
|
||||
import { Heading } from '../components/Heading';
|
||||
import { CartStore } from '../store';
|
||||
import { getCart } from '../store/Selectors';
|
||||
|
||||
import styles from './Cart.module.scss';
|
||||
|
||||
const Cart = () => {
|
||||
const cart = useStoreState(CartStore, getCart);
|
||||
const [amount, setAmount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
var total = 0.0;
|
||||
cart.forEach((product) => (total += product.price));
|
||||
setAmount(total.toFixed(2));
|
||||
}, [cart]);
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Cart</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Cart</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonGrid>
|
||||
{cart.length < 1 && (
|
||||
<IonRow className={styles.emptyCartContainer}>
|
||||
<IonCol size="10" className="ion-text-center">
|
||||
<div className={styles.text}>
|
||||
<img src="/assets/DemoShopAppUi/cart.png" alt="no cart" />
|
||||
<h1>Hang on there!</h1>
|
||||
<p>Your cart is empty</p>
|
||||
<IonButton color="primary" routerLink={'/demo-shop-app-ui/home'}>
|
||||
Shop now →
|
||||
</IonButton>
|
||||
</div>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
)}
|
||||
|
||||
{cart.length > 0 && (
|
||||
<>
|
||||
<IonRow className={styles.cartContainer}>
|
||||
<IonCol size="12">
|
||||
<Heading
|
||||
heading={`You have ${cart.length} products in your cart. Your total comes to £${amount}.`}
|
||||
/>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow>
|
||||
{cart.map((product, index) => {
|
||||
return <CartProduct product={product} key={index} />;
|
||||
})}
|
||||
</IonRow>
|
||||
</>
|
||||
)}
|
||||
</IonGrid>
|
||||
|
||||
{cart.length > 0 && (
|
||||
<IonGrid className={styles.bottom}>
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<IonButton color="primary" expand="block">
|
||||
Checkout (£{amount})
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
)}
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Cart;
|
@@ -0,0 +1,30 @@
|
||||
.emptyCartContainer {
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
margin-top: 2rem;
|
||||
|
||||
.text {
|
||||
|
||||
|
||||
color: rgb(78, 78, 78);
|
||||
padding: 1rem;
|
||||
border: 2px solid rgb(231, 231, 231);
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
|
||||
width: 100%;
|
||||
border-top: 2px solid var(--ion-color-primary);
|
||||
background-color: white;
|
||||
position: fixed;
|
||||
bottom: 0rem;
|
||||
padding-left: 1.2rem;
|
||||
padding-right: 1.2rem;
|
||||
}
|
@@ -0,0 +1,90 @@
|
||||
import { IonBackButton, IonBreadcrumb, IonBreadcrumbs, IonButtons, IonCol, IonContent, IonGrid, IonHeader, IonPage, IonRow, IonSearchbar, IonTitle, IonToolbar, useIonViewWillEnter, useIonViewWillLeave, IonRouterLink, useIonModal } from '@ionic/react';
|
||||
import { useStoreState } from 'pullstate';
|
||||
import { useRef } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { Product } from '../components/Product';
|
||||
import { ProductModal } from '../components/ProductModal';
|
||||
import { ProductStore } from '../store';
|
||||
import { getCategoryProducts } from '../store/Selectors';
|
||||
import { capitalizeWords } from '../utils';
|
||||
|
||||
import styles from "./Category.module.scss";
|
||||
|
||||
const Category = () => {
|
||||
|
||||
const pageRef = useRef();
|
||||
const { name } = useParams();
|
||||
const products = useStoreState(ProductStore, getCategoryProducts(name));
|
||||
const [ selectedProduct, setSelectedProduct ] = useState(false);
|
||||
|
||||
const handleShowModal = product => {
|
||||
|
||||
setSelectedProduct(product);
|
||||
present({
|
||||
|
||||
cssClass: "product-modal",
|
||||
presentingElement: pageRef.current
|
||||
});
|
||||
}
|
||||
|
||||
const [ present, dismiss ] = useIonModal(ProductModal, {
|
||||
|
||||
dismiss: () => dismiss(),
|
||||
product: selectedProduct
|
||||
});
|
||||
|
||||
useIonViewWillEnter(() => {
|
||||
|
||||
document.querySelector("ion-tab-bar").style.display = "none";
|
||||
});
|
||||
|
||||
useIonViewWillLeave(() => {
|
||||
|
||||
document.querySelector("ion-tab-bar").style.display = "";
|
||||
})
|
||||
|
||||
return (
|
||||
<IonPage ref={ pageRef }>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonBackButton />
|
||||
</IonButtons>
|
||||
<IonTitle>{ capitalizeWords(name) }</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
{/* <IonTitle size="large">{ capitalizeWords(name) }</IonTitle> */}
|
||||
<IonBreadcrumbs>
|
||||
<IonBreadcrumb color="dark">
|
||||
<IonRouterLink routerLink="/home" routerDirection="back" color="dark">Shop</IonRouterLink>
|
||||
</IonBreadcrumb>
|
||||
<IonBreadcrumb color="primary" active>{ capitalizeWords(name) }</IonBreadcrumb>
|
||||
</IonBreadcrumbs>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonGrid className="ion-padding">
|
||||
|
||||
<IonRow className={ styles.searchContainer }>
|
||||
<IonCol size="12">
|
||||
<IonSearchbar animated placeholder="Search for a product" />
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow>
|
||||
{ products.map((product, index) => {
|
||||
|
||||
return <Product product={ product } key={ `product_${ index }` } click={ () => handleShowModal(product) } />;
|
||||
})}
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Category;
|
@@ -0,0 +1,6 @@
|
||||
.searchContainer {
|
||||
|
||||
margin-left: -1.2rem;
|
||||
margin-top: -1.5rem;
|
||||
margin-bottom: -0.5rem;
|
||||
}
|
104
03_source/mobile/src/pages/DemoShopAppUi/AppPages/Home.jsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonContent,
|
||||
IonGrid,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonModal,
|
||||
IonPage,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonModal,
|
||||
useIonRouter,
|
||||
} from '@ionic/react';
|
||||
import { useRef } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { CategoriesModal } from '../components/CategoriesModal';
|
||||
import { Heading } from '../components/Heading';
|
||||
import { Offer } from '../components/Offer';
|
||||
import { PopularCategories } from '../components/PopularCategories';
|
||||
import { ProductModal } from '../components/ProductModal';
|
||||
import { TrendingProducts } from '../components/TrendingProducts';
|
||||
import { chevronBackOutline, refreshOutline } from 'ionicons/icons';
|
||||
|
||||
const Home = () => {
|
||||
const router = useIonRouter();
|
||||
|
||||
const pageRef = useRef();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [selectedProduct, setSelectedProduct] = useState(false);
|
||||
|
||||
const handleShowModal = (product) => {
|
||||
setSelectedProduct(product);
|
||||
present({
|
||||
cssClass: 'product-modal',
|
||||
presentingElement: pageRef.current,
|
||||
});
|
||||
};
|
||||
|
||||
const [present, dismiss] = useIonModal(ProductModal, {
|
||||
dismiss: () => dismiss(),
|
||||
product: selectedProduct,
|
||||
});
|
||||
|
||||
function handleBackClick() {
|
||||
router.goBack();
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage ref={pageRef}>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Ionic Shop</IonTitle>
|
||||
|
||||
<IonButtons slot="start">
|
||||
<IonButton onClick={() => handleBackClick()}>
|
||||
<IonIcon icon={chevronBackOutline} color="primary" />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Ionic Shop 123321</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonGrid>
|
||||
<Offer
|
||||
image="/assets/DemoShopAppUi/shop.png"
|
||||
heading="Autumn Sale!"
|
||||
text="30% off all products"
|
||||
/>
|
||||
<Heading
|
||||
heading="Popular Categories"
|
||||
buttonClick={() => setShowModal(true)}
|
||||
buttonText="See all"
|
||||
/>
|
||||
<PopularCategories />
|
||||
<Offer
|
||||
image="/assets/DemoShopAppUi/shop.png"
|
||||
heading="Buy now, pay later."
|
||||
text="Spread the cost of your purchase"
|
||||
/>
|
||||
<Heading heading="Trending Products" />
|
||||
<TrendingProducts click={handleShowModal} />
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
|
||||
<IonModal
|
||||
isOpen={showModal}
|
||||
onDidDismiss={() => setShowModal(false)}
|
||||
breakpoints={[0, 0.27, 0.5, 1]}
|
||||
initialBreakpoint={0.27}
|
||||
backdropBreakpoint={0.5}
|
||||
>
|
||||
<CategoriesModal close={() => setShowModal(false)} />
|
||||
</IonModal>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
@@ -0,0 +1,22 @@
|
||||
import { IonBadge, IonCol, IonIcon, IonNote } from "@ionic/react";
|
||||
import { star } from "ionicons/icons";
|
||||
import styles from "./CartProduct.module.scss";
|
||||
|
||||
export const CartProduct = ({ product, click, fromHome = false }) => {
|
||||
|
||||
return (
|
||||
|
||||
<IonCol size="12" onClick={ click } className={ !fromHome ? "animate__animated animate__faster animate__slideInRight" : null }>
|
||||
<div className={ styles.productContainer }>
|
||||
|
||||
<div className={ styles.productInfo }>
|
||||
<div>
|
||||
<IonBadge color="primary">£{ product.price.toFixed(2) }</IonBadge>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className={ `${ styles.productTitle } truncate` }>{ product.title }</h1>
|
||||
<div style={{ backgroundImage: `url(${ product.image })` }} className={ styles.coverImage } />
|
||||
</div>
|
||||
</IonCol>
|
||||
);
|
||||
}
|