Compare commits
12 Commits
develop/mo
...
develop/mo
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9aeb58379d | ||
![]() |
6419567005 | ||
![]() |
e2076fe67b | ||
![]() |
766720e075 | ||
![]() |
19af60c410 | ||
![]() |
ed95621b2f | ||
![]() |
2258fd8fb9 | ||
![]() |
0f674badd9 | ||
![]() |
bc731ea2b8 | ||
![]() |
c11dba6297 | ||
![]() |
b9fbe4e476 | ||
![]() |
0d844eed3f |
@@ -0,0 +1,21 @@
|
||||
import { IonCol, IonRouterLink, IonRow } from '@ionic/react';
|
||||
|
||||
interface ActionProps {
|
||||
message: string;
|
||||
text: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export const Action = (props: ActionProps): React.JSX.Element => (
|
||||
<IonRow className="ion-text-center ion-justify-content-center">
|
||||
<IonCol size="12">
|
||||
<p>
|
||||
{props.message}
|
||||
<IonRouterLink className="custom-link" routerLink={props.link}>
|
||||
{' '}
|
||||
{props.text} →
|
||||
</IonRouterLink>
|
||||
</p>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
);
|
@@ -0,0 +1,29 @@
|
||||
.field:not(:last-child) {
|
||||
margin-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
.field {
|
||||
ion-label {
|
||||
padding-left: 0.2rem;
|
||||
padding-right: 0.5rem;
|
||||
color: #d3a6c7;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
color: rgb(236, 149, 35);
|
||||
}
|
||||
}
|
||||
}
|
||||
.customInput {
|
||||
--background: #834e76;
|
||||
--padding-bottom: 1rem;
|
||||
--padding-top: 1rem;
|
||||
--padding-start: 1rem;
|
||||
--padding-end: 1rem;
|
||||
border-radius: 10px;
|
||||
margin-top: 0.25rem;
|
||||
transition: all 0.2s linear;
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
import { IonInput, IonLabel } from '@ionic/react';
|
||||
import styles from './CustomField.module.scss';
|
||||
|
||||
interface FieldType {
|
||||
id: string;
|
||||
label: string;
|
||||
input: {
|
||||
props: any;
|
||||
state: any;
|
||||
};
|
||||
}
|
||||
|
||||
interface ErrorType {
|
||||
id: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface CustomFieldProps {
|
||||
field: FieldType;
|
||||
errors?: ErrorType[];
|
||||
}
|
||||
|
||||
const CustomField = ({ field, errors }: CustomFieldProps): React.JSX.Element => {
|
||||
const error = errors && errors.filter((e) => e.id === field.id)[0];
|
||||
const errorMessage = error && errors.filter((e) => e.id === field.id)[0].message;
|
||||
|
||||
return (
|
||||
<div className={styles.field}>
|
||||
<IonLabel className={styles.fieldLabel}>
|
||||
{field.label}
|
||||
{error && <p className="animate__animated animate__bounceIn">{errorMessage}</p>}
|
||||
</IonLabel>
|
||||
<IonInput className={styles.customInput} {...field.input.props} {...field.input.state} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomField;
|
@@ -0,0 +1,13 @@
|
||||
export const Wave = (): React.JSX.Element => (
|
||||
<svg
|
||||
style={{ marginBottom: '-0.5rem' }}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1440 320"
|
||||
>
|
||||
<path
|
||||
fill="#7a506f"
|
||||
fill-opacity="1"
|
||||
d="M0,288L40,277.3C80,267,160,245,240,224C320,203,400,181,480,176C560,171,640,181,720,181.3C800,181,880,171,960,144C1040,117,1120,75,1200,58.7C1280,43,1360,53,1400,58.7L1440,64L1440,320L1400,320C1360,320,1280,320,1200,320C1120,320,1040,320,960,320C880,320,800,320,720,320C640,320,560,320,480,320C400,320,320,320,240,320C160,320,80,320,40,320L0,320Z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
82
03_source/mobile/src/pages/DemoReactLogin/data/fields.tsx
Normal file
82
03_source/mobile/src/pages/DemoReactLogin/data/fields.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useFormInput } from "./utils";
|
||||
|
||||
export const useSignupFields = () => {
|
||||
|
||||
return [
|
||||
{
|
||||
id: "name",
|
||||
label: "Name",
|
||||
required: true,
|
||||
input: {
|
||||
|
||||
props: {
|
||||
|
||||
type: "text",
|
||||
placeholder: "Joe Bloggs"
|
||||
},
|
||||
state: useFormInput("")
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "email",
|
||||
label: "Email",
|
||||
required: true,
|
||||
input: {
|
||||
|
||||
props: {
|
||||
|
||||
type: "email",
|
||||
placeholder: "joe@bloggs.com"
|
||||
},
|
||||
state: useFormInput("")
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "password",
|
||||
label: "Password",
|
||||
required: true,
|
||||
input: {
|
||||
|
||||
props: {
|
||||
|
||||
type: "password",
|
||||
placeholder: "*********"
|
||||
},
|
||||
state: useFormInput("")
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
export const useLoginFields = () => {
|
||||
|
||||
return [
|
||||
|
||||
{
|
||||
id: "email",
|
||||
label: "Email",
|
||||
required: true,
|
||||
input: {
|
||||
|
||||
props: {
|
||||
type: "email",
|
||||
placeholder: "joe@bloggs.com"
|
||||
},
|
||||
state: useFormInput("")
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "password",
|
||||
label: "Password",
|
||||
required: true,
|
||||
input: {
|
||||
|
||||
props: {
|
||||
type: "password",
|
||||
placeholder: "*******"
|
||||
},
|
||||
state: useFormInput("")
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
46
03_source/mobile/src/pages/DemoReactLogin/data/utils.tsx
Normal file
46
03_source/mobile/src/pages/DemoReactLogin/data/utils.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export const useFormInput = (initialValue = "") => {
|
||||
|
||||
const [ value, setValue ] = useState(initialValue);
|
||||
|
||||
const handleChange = async e => {
|
||||
|
||||
const tempValue = await e.currentTarget.value;
|
||||
setValue(tempValue);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
value,
|
||||
reset: (newValue) => setValue(newValue),
|
||||
onIonChange: handleChange,
|
||||
onKeyUp: handleChange
|
||||
};
|
||||
}
|
||||
|
||||
export const validateForm = fields => {
|
||||
|
||||
let errors = [];
|
||||
|
||||
fields.forEach(field => {
|
||||
|
||||
if (field.required) {
|
||||
|
||||
const fieldValue = field.input.state.value;
|
||||
|
||||
if (fieldValue === "") {
|
||||
|
||||
const error = {
|
||||
|
||||
id: field.id,
|
||||
message: `Please check your ${ field.id }`,
|
||||
};
|
||||
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
@@ -1,28 +1,33 @@
|
||||
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
|
||||
import { IonRouterOutlet, 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';
|
||||
import './theme/variables.scss';
|
||||
|
||||
import './style.scss';
|
||||
import Home from './pages/Home';
|
||||
import Login from './pages/Login';
|
||||
import Signup from './pages/Signup';
|
||||
|
||||
function DemoReactLogin() {
|
||||
return (
|
||||
<IonTabs>
|
||||
<IonRouterOutlet>
|
||||
<Route exact path="/demo-react-login/tab1">
|
||||
<Tab1 />
|
||||
</Route>
|
||||
<Route exact path="/demo-react-login/tab2">
|
||||
<Tab2 />
|
||||
<Route exact path="/demo-react-login/home">
|
||||
<Home />
|
||||
</Route>
|
||||
|
||||
<Redirect exact path="/demo-react-login" to="/demo-react-login/tab1" />
|
||||
<Route exact path="/demo-react-login/signup">
|
||||
<Signup />
|
||||
</Route>
|
||||
|
||||
<Route exact path="/demo-react-login/login">
|
||||
<Login />
|
||||
</Route>
|
||||
|
||||
<Redirect exact path="/demo-react-login" to="/demo-react-login/home" />
|
||||
</IonRouterOutlet>
|
||||
|
||||
{/* */}
|
||||
{/*
|
||||
<IonTabBar slot="bottom">
|
||||
<IonTabButton tab="tab1" href="/demo-react-login/tab1">
|
||||
<IonIcon icon={cloudOutline} />
|
||||
@@ -33,6 +38,7 @@ function DemoReactLogin() {
|
||||
<IonLabel>Search</IonLabel>
|
||||
</IonTabButton>
|
||||
</IonTabBar>
|
||||
*/}
|
||||
</IonTabs>
|
||||
);
|
||||
}
|
||||
|
9
03_source/mobile/src/pages/DemoReactLogin/module.d.ts
vendored
Normal file
9
03_source/mobile/src/pages/DemoReactLogin/module.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
declare module '*.module.css' {
|
||||
const classes: { readonly [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module '*.module.scss' {
|
||||
const classes: { readonly [key: string]: string };
|
||||
export default classes;
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
.homePage {
|
||||
ion-header {
|
||||
ion-img {
|
||||
border-bottom: 3px solid rgb(236, 149, 35);
|
||||
}
|
||||
}
|
||||
|
||||
ion-footer {
|
||||
background-color: #7c3b6a;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.getStarted {
|
||||
height: 100%;
|
||||
background-color: #ffffff;
|
||||
background-image:
|
||||
radial-gradient(#b8b8b8 1px, transparent 1px), radial-gradient(#b8b8b8 1px, #ffffff 1px);
|
||||
background-size: 40px 40px;
|
||||
background-position:
|
||||
0 0,
|
||||
20px 20px;
|
||||
|
||||
ion-card-title {
|
||||
color: black !important;
|
||||
letter-spacing: -0.08rem;
|
||||
font-weight: 900 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin-top: 7rem;
|
||||
}
|
||||
|
||||
.getStartedButton {
|
||||
font-size: 1.2rem;
|
||||
margin-top: 1rem;
|
||||
}
|
59
03_source/mobile/src/pages/DemoReactLogin/pages/Home.tsx
Normal file
59
03_source/mobile/src/pages/DemoReactLogin/pages/Home.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonCardTitle,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonFooter,
|
||||
IonGrid,
|
||||
IonHeader,
|
||||
IonImg,
|
||||
IonPage,
|
||||
IonRouterLink,
|
||||
IonRow,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
import { Action } from '../components/Action';
|
||||
import styles from './Home.module.scss';
|
||||
|
||||
const Home = (): React.JSX.Element => {
|
||||
return (
|
||||
<IonPage className={styles.homePage}>
|
||||
<IonHeader>
|
||||
{/* <IonToolbar className="ion-no-margin ion-no-padding"> */}
|
||||
<IonImg src="/assets/DemoReactLogin/login2.jpeg" />
|
||||
{/* </IonToolbar> */}
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<div className={styles.getStarted}>
|
||||
<IonGrid>
|
||||
<IonRow className={`ion-text-center ion-justify-content-center ${styles.heading}`}>
|
||||
<IonCol size="11" className={styles.headingText}>
|
||||
<IonCardTitle>
|
||||
Join millions of other people discovering their creative side
|
||||
</IonCardTitle>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow className={`ion-text-center ion-justify-content-center`}>
|
||||
<IonRouterLink routerLink="/signup" className="custom-link">
|
||||
<IonCol size="11">
|
||||
<IonButton className={`${styles.getStartedButton} custom-button`}>
|
||||
Get started →
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRouterLink>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</div>
|
||||
</IonContent>
|
||||
|
||||
<IonFooter>
|
||||
<IonGrid>
|
||||
<Action message="Already got an account?" text="Login" link="/login" />
|
||||
</IonGrid>
|
||||
</IonFooter>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
@@ -0,0 +1,17 @@
|
||||
.loginPage {
|
||||
ion-toolbar {
|
||||
--border-style: none;
|
||||
--border-color: transparent;
|
||||
--padding-top: 1rem;
|
||||
--padding-bottom: 1rem;
|
||||
--padding-start: 1rem;
|
||||
--padding-end: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.headingText {
|
||||
h5 {
|
||||
margin-top: 0.2rem;
|
||||
color: #d3a6c7;
|
||||
}
|
||||
}
|
112
03_source/mobile/src/pages/DemoReactLogin/pages/Login.tsx
Normal file
112
03_source/mobile/src/pages/DemoReactLogin/pages/Login.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
IonBackButton,
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonCardTitle,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonFooter,
|
||||
IonGrid,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonPage,
|
||||
IonRouterLink,
|
||||
IonRow,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
import styles from './Login.module.scss';
|
||||
|
||||
import { arrowBack, shapesOutline } from 'ionicons/icons';
|
||||
import CustomField from '../components/CustomField';
|
||||
import { useLoginFields } from '../data/fields';
|
||||
import { Action } from '../components/Action';
|
||||
import { Wave } from '../components/Wave';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { validateForm } from '../data/utils';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
interface FieldType {
|
||||
id: string;
|
||||
label: string;
|
||||
input: {
|
||||
props: any;
|
||||
state: any;
|
||||
};
|
||||
}
|
||||
|
||||
interface ErrorType {
|
||||
id: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const Login = (): React.JSX.Element => {
|
||||
const params = useParams();
|
||||
|
||||
const fields: FieldType[] = useLoginFields();
|
||||
const [errors, setErrors] = useState<ErrorType[] | false>(false);
|
||||
|
||||
const login = (): void => {
|
||||
const errors = validateForm(fields);
|
||||
setErrors(errors);
|
||||
|
||||
if (!errors.length) {
|
||||
// Submit your form here
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
fields.forEach((field) => field.input.state.reset(''));
|
||||
setErrors(false);
|
||||
};
|
||||
}, [params]);
|
||||
|
||||
return (
|
||||
<IonPage className={styles.loginPage}>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonBackButton icon={arrowBack} text="" className="custom-back" />
|
||||
</IonButtons>
|
||||
|
||||
<IonButtons slot="end">
|
||||
<IonButton className="custom-button">
|
||||
<IonIcon icon={shapesOutline} />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonGrid className="ion-padding">
|
||||
<IonRow>
|
||||
<IonCol size="12" className={styles.headingText}>
|
||||
<IonCardTitle>Log in</IonCardTitle>
|
||||
<h5>Welcome back, hope you're doing well</h5>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow className="ion-margin-top ion-padding-top">
|
||||
<IonCol size="12">
|
||||
{fields.map((field) => {
|
||||
return <CustomField field={field} errors={errors} />;
|
||||
})}
|
||||
|
||||
<IonButton className="custom-button" expand="block" onClick={login}>
|
||||
Login
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
|
||||
<IonFooter>
|
||||
<IonGrid className="ion-no-margin ion-no-padding">
|
||||
<Action message="Don't have an account?" text="Sign up" link="/signup" />
|
||||
<Wave />
|
||||
</IonGrid>
|
||||
</IonFooter>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
@@ -0,0 +1,17 @@
|
||||
.signupPage {
|
||||
ion-toolbar {
|
||||
--border-style: none;
|
||||
--border-color: transparent;
|
||||
--padding-top: 1rem;
|
||||
--padding-bottom: 1rem;
|
||||
--padding-start: 1rem;
|
||||
--padding-end: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.headingText {
|
||||
h5 {
|
||||
margin-top: 0.2rem;
|
||||
color: #d3a6c7;
|
||||
}
|
||||
}
|
111
03_source/mobile/src/pages/DemoReactLogin/pages/Signup.tsx
Normal file
111
03_source/mobile/src/pages/DemoReactLogin/pages/Signup.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
IonBackButton,
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonCardTitle,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonFooter,
|
||||
IonGrid,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonPage,
|
||||
IonRouterLink,
|
||||
IonRow,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
import styles from './Signup.module.scss';
|
||||
|
||||
import { arrowBack, shapesOutline } from 'ionicons/icons';
|
||||
import CustomField from '../components/CustomField';
|
||||
import { useSignupFields } from '../data/fields';
|
||||
import { Action } from '../components/Action';
|
||||
import { Wave } from '../components/Wave';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { validateForm } from '../data/utils';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
interface FieldType {
|
||||
id: string;
|
||||
label: string;
|
||||
input: {
|
||||
props: any;
|
||||
state: any;
|
||||
};
|
||||
}
|
||||
|
||||
interface ErrorType {
|
||||
id: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const Signup = (): React.JSX.Element => {
|
||||
const params = useParams();
|
||||
const fields: FieldType[] = useSignupFields();
|
||||
const [errors, setErrors] = useState<ErrorType[] | false>(false);
|
||||
|
||||
const createAccount = (): void => {
|
||||
const errors = validateForm(fields);
|
||||
setErrors(errors);
|
||||
|
||||
if (!errors.length) {
|
||||
// Submit your form here
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
fields.forEach((field) => field.input.state.reset(''));
|
||||
setErrors(false);
|
||||
};
|
||||
}, [params]);
|
||||
|
||||
return (
|
||||
<IonPage className={styles.signupPage}>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonBackButton icon={arrowBack} text="" className="custom-back" />
|
||||
</IonButtons>
|
||||
|
||||
<IonButtons slot="end">
|
||||
<IonButton className="custom-button">
|
||||
<IonIcon icon={shapesOutline} />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonGrid className="ion-padding">
|
||||
<IonRow>
|
||||
<IonCol size="12" className={styles.headingText}>
|
||||
<IonCardTitle>Sign up</IonCardTitle>
|
||||
<h5>Lets get to know each other</h5>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow className="ion-margin-top ion-padding-top">
|
||||
<IonCol size="12">
|
||||
{fields.map((field) => {
|
||||
return <CustomField field={field} errors={errors} />;
|
||||
})}
|
||||
|
||||
<IonButton className="custom-button" expand="block" onClick={createAccount}>
|
||||
Create account
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
|
||||
<IonFooter>
|
||||
<IonGrid className="ion-no-margin ion-no-padding">
|
||||
<Action message="Already got an account?" text="Login" link="/login" />
|
||||
<Wave />
|
||||
</IonGrid>
|
||||
</IonFooter>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Signup;
|
@@ -0,0 +1,19 @@
|
||||
import { Store } from "pullstate";
|
||||
|
||||
const AccountStore = new Store({
|
||||
|
||||
logged_in: false,
|
||||
coffee_ids: []
|
||||
});
|
||||
|
||||
export default AccountStore;
|
||||
|
||||
// export const addToCart = (coffeeID) => {
|
||||
|
||||
// CartStore.update(s => { s.coffee_ids = [ ...s.coffee_ids, `${ parseInt(coffeeID) }` ]; });
|
||||
// }
|
||||
|
||||
// export const removeFromCart = coffeeIndex => {
|
||||
|
||||
// CartStore.update(s => { s.coffee_ids.splice(coffeeIndex, 1) });
|
||||
// }
|
@@ -0,0 +1,13 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
const getState = state => state;
|
||||
|
||||
// General getters
|
||||
// export const getCoffees = createSelector(getState, state => state.coffees);
|
||||
// export const getOffers = createSelector(getState, state => state.offers);
|
||||
// export const getCoffeeSizes = createSelector(getState, state => state.sizes);
|
||||
// export const getCartCoffees = createSelector(getState, state => state.coffee_ids);
|
||||
// export const getFavouriteCoffees = createSelector(getState, state => state.coffee_ids);
|
||||
|
||||
// // More specific getters
|
||||
// export const getCoffee = id => createSelector(getState, state => state.coffees.filter(c => parseInt(c.id) === parseInt(id))[0]);
|
@@ -0,0 +1 @@
|
||||
export { default as AccountStore } from "./AccountStore";
|
@@ -1,103 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ import Info from './pages/Info';
|
||||
|
||||
function DemoReactMarvelApp() {
|
||||
return (
|
||||
<IonTabs>
|
||||
<IonTabs className="demo-react-marvel-app">
|
||||
<IonRouterOutlet>
|
||||
<Route exact path="/demo-react-marvel-app/home">
|
||||
<Home />
|
||||
|
@@ -1,32 +1,30 @@
|
||||
.character-container {
|
||||
|
||||
.demo-react-marvel-app {
|
||||
.character-container {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
.character-name-container {
|
||||
|
||||
}
|
||||
.character-name-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
position:absolute;
|
||||
position: absolute;
|
||||
z-index: 99999;
|
||||
background-color: rgba(0,0,0,0.7);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.character-name-container ion-icon {
|
||||
}
|
||||
|
||||
.character-name-container ion-icon {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.character-name-container ion-label {
|
||||
}
|
||||
|
||||
.character-name-container ion-label {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
@@ -15,7 +15,7 @@ import {
|
||||
useIonRouter,
|
||||
} from '@ionic/react';
|
||||
|
||||
import './Home.css';
|
||||
import './Home.scss';
|
||||
import {
|
||||
addOutline,
|
||||
chevronBackOutline,
|
||||
|
@@ -1,59 +1,52 @@
|
||||
.characterContainer {
|
||||
|
||||
position: relative;
|
||||
text-align: center;
|
||||
color: white;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
ion-item {
|
||||
--padding-start: 0;
|
||||
--inner-padding-end: 0;
|
||||
--padding-start: 0;
|
||||
--inner-padding-end: 0;
|
||||
}
|
||||
|
||||
ion-label {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.characterNameContainer {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
z-index: 99999;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
z-index: 99999;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.characterNameContainer ion-label {
|
||||
|
||||
font-size: 1rem !important;
|
||||
font-weight: 500;
|
||||
font-size: 1rem !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.characterStats {
|
||||
|
||||
text-align: center;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
text-align: center;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.characterStat {
|
||||
|
||||
background-color: var(--ion-color-primary);
|
||||
padding: 1rem;
|
||||
background-color: var(--ion-color-primary);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.characterStat ion-card-title {
|
||||
|
||||
font-size: 1rem;
|
||||
--color: white;
|
||||
font-size: 1rem;
|
||||
--color: white;
|
||||
}
|
||||
|
||||
.characterStat ion-card-subtitle {
|
||||
|
||||
font-size: 1rem;
|
||||
--color: white;
|
||||
}
|
||||
font-size: 1rem;
|
||||
--color: white;
|
||||
}
|
||||
|
@@ -1,245 +0,0 @@
|
||||
.demo-react-marvel-app {
|
||||
/* Ionic Variables and Theming. For more info, please see:
|
||||
http://ionicframework.com/docs/theming/ */
|
||||
|
||||
/** Ionic CSS Variables **/
|
||||
:root {
|
||||
/** primary **/
|
||||
--ion-color-primary: #3880ff;
|
||||
--ion-color-primary-rgb: 56, 128, 255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #3171e0;
|
||||
--ion-color-primary-tint: #4c8dff;
|
||||
|
||||
/** secondary **/
|
||||
--ion-color-secondary: #3dc2ff;
|
||||
--ion-color-secondary-rgb: 61, 194, 255;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #36abe0;
|
||||
--ion-color-secondary-tint: #50c8ff;
|
||||
|
||||
/** tertiary **/
|
||||
--ion-color-tertiary: #5260ff;
|
||||
--ion-color-tertiary-rgb: 82, 96, 255;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-tertiary-shade: #4854e0;
|
||||
--ion-color-tertiary-tint: #6370ff;
|
||||
|
||||
/** success **/
|
||||
--ion-color-success: #2dd36f;
|
||||
--ion-color-success-rgb: 45, 211, 111;
|
||||
--ion-color-success-contrast: #ffffff;
|
||||
--ion-color-success-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-success-shade: #28ba62;
|
||||
--ion-color-success-tint: #42d77d;
|
||||
|
||||
/** warning **/
|
||||
--ion-color-warning: #ffc409;
|
||||
--ion-color-warning-rgb: 255, 196, 9;
|
||||
--ion-color-warning-contrast: #000000;
|
||||
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-warning-shade: #e0ac08;
|
||||
--ion-color-warning-tint: #ffca22;
|
||||
|
||||
/** danger **/
|
||||
--ion-color-danger: #eb445a;
|
||||
--ion-color-danger-rgb: 235, 68, 90;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #cf3c4f;
|
||||
--ion-color-danger-tint: #ed576b;
|
||||
|
||||
/** dark **/
|
||||
--ion-color-dark: #222428;
|
||||
--ion-color-dark-rgb: 34, 36, 40;
|
||||
--ion-color-dark-contrast: #ffffff;
|
||||
--ion-color-dark-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-dark-shade: #1e2023;
|
||||
--ion-color-dark-tint: #383a3e;
|
||||
|
||||
/** medium **/
|
||||
--ion-color-medium: #92949c;
|
||||
--ion-color-medium-rgb: 146, 148, 156;
|
||||
--ion-color-medium-contrast: #ffffff;
|
||||
--ion-color-medium-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-medium-shade: #808289;
|
||||
--ion-color-medium-tint: #9d9fa6;
|
||||
|
||||
/** light **/
|
||||
--ion-color-light: #f4f5f8;
|
||||
--ion-color-light-rgb: 244, 245, 248;
|
||||
--ion-color-light-contrast: #000000;
|
||||
--ion-color-light-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-light-shade: #d7d8da;
|
||||
--ion-color-light-tint: #f5f6f9;
|
||||
}
|
||||
|
||||
/* @media (prefers-color-scheme: dark) { */
|
||||
/*
|
||||
* Dark Colors
|
||||
* -------------------------------------------
|
||||
*/
|
||||
|
||||
body {
|
||||
--ion-color-primary: #ee0000;
|
||||
--ion-color-primary-rgb: 66, 140, 255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #3a7be0;
|
||||
--ion-color-primary-tint: #5598ff;
|
||||
|
||||
--ion-color-secondary: #50c8ff;
|
||||
--ion-color-secondary-rgb: 80, 200, 255;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #46b0e0;
|
||||
--ion-color-secondary-tint: #62ceff;
|
||||
|
||||
--ion-color-tertiary: #6a64ff;
|
||||
--ion-color-tertiary-rgb: 106, 100, 255;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-tertiary-shade: #5d58e0;
|
||||
--ion-color-tertiary-tint: #7974ff;
|
||||
|
||||
--ion-color-success: #2fdf75;
|
||||
--ion-color-success-rgb: 47, 223, 117;
|
||||
--ion-color-success-contrast: #000000;
|
||||
--ion-color-success-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-success-shade: #29c467;
|
||||
--ion-color-success-tint: #44e283;
|
||||
|
||||
--ion-color-warning: #ffd534;
|
||||
--ion-color-warning-rgb: 255, 213, 52;
|
||||
--ion-color-warning-contrast: #000000;
|
||||
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-warning-shade: #e0bb2e;
|
||||
--ion-color-warning-tint: #ffd948;
|
||||
|
||||
--ion-color-danger: #ee0000;
|
||||
--ion-color-danger-rgb: 255, 73, 97;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #e04055;
|
||||
--ion-color-danger-tint: #ff5b71;
|
||||
|
||||
--ion-color-dark: #f4f5f8;
|
||||
--ion-color-dark-rgb: 244, 245, 248;
|
||||
--ion-color-dark-contrast: #000000;
|
||||
--ion-color-dark-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-dark-shade: #d7d8da;
|
||||
--ion-color-dark-tint: #f5f6f9;
|
||||
|
||||
--ion-color-medium: #989aa2;
|
||||
--ion-color-medium-rgb: 152, 154, 162;
|
||||
--ion-color-medium-contrast: #000000;
|
||||
--ion-color-medium-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-medium-shade: #86888f;
|
||||
--ion-color-medium-tint: #a2a4ab;
|
||||
|
||||
--ion-color-light: #222428;
|
||||
--ion-color-light-rgb: 34, 36, 40;
|
||||
--ion-color-light-contrast: #ffffff;
|
||||
--ion-color-light-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-light-shade: #1e2023;
|
||||
--ion-color-light-tint: #383a3e;
|
||||
}
|
||||
|
||||
/*
|
||||
* iOS Dark Theme
|
||||
* -------------------------------------------
|
||||
*/
|
||||
|
||||
.ios body {
|
||||
--ion-background-color: #000000;
|
||||
--ion-background-color-rgb: 0, 0, 0;
|
||||
|
||||
--ion-text-color: #ffffff;
|
||||
--ion-text-color-rgb: 255, 255, 255;
|
||||
|
||||
--ion-color-step-50: #0d0d0d;
|
||||
--ion-color-step-100: #1a1a1a;
|
||||
--ion-color-step-150: #262626;
|
||||
--ion-color-step-200: #333333;
|
||||
--ion-color-step-250: #404040;
|
||||
--ion-color-step-300: #4d4d4d;
|
||||
--ion-color-step-350: #595959;
|
||||
--ion-color-step-400: #666666;
|
||||
--ion-color-step-450: #737373;
|
||||
--ion-color-step-500: #808080;
|
||||
--ion-color-step-550: #8c8c8c;
|
||||
--ion-color-step-600: #999999;
|
||||
--ion-color-step-650: #a6a6a6;
|
||||
--ion-color-step-700: #b3b3b3;
|
||||
--ion-color-step-750: #bfbfbf;
|
||||
--ion-color-step-800: #cccccc;
|
||||
--ion-color-step-850: #d9d9d9;
|
||||
--ion-color-step-900: #e6e6e6;
|
||||
--ion-color-step-950: #f2f2f2;
|
||||
|
||||
--ion-item-background: #000000;
|
||||
|
||||
--ion-card-background: #1c1c1d;
|
||||
}
|
||||
|
||||
.ios ion-modal {
|
||||
--ion-background-color: var(--ion-color-step-100);
|
||||
--ion-toolbar-background: var(--ion-color-step-150);
|
||||
--ion-toolbar-border-color: var(--ion-color-step-250);
|
||||
}
|
||||
|
||||
/*
|
||||
* Material Design Dark Theme
|
||||
* -------------------------------------------
|
||||
*/
|
||||
|
||||
.md body {
|
||||
--ion-background-color: #121212;
|
||||
--ion-background-color-rgb: 18, 18, 18;
|
||||
|
||||
--ion-text-color: #ffffff;
|
||||
--ion-text-color-rgb: 255, 255, 255;
|
||||
|
||||
--ion-border-color: #222222;
|
||||
|
||||
--ion-color-step-50: #1e1e1e;
|
||||
--ion-color-step-100: #2a2a2a;
|
||||
--ion-color-step-150: #363636;
|
||||
--ion-color-step-200: #414141;
|
||||
--ion-color-step-250: #4d4d4d;
|
||||
--ion-color-step-300: #595959;
|
||||
--ion-color-step-350: #656565;
|
||||
--ion-color-step-400: #717171;
|
||||
--ion-color-step-450: #7d7d7d;
|
||||
--ion-color-step-500: #898989;
|
||||
--ion-color-step-550: #949494;
|
||||
--ion-color-step-600: #a0a0a0;
|
||||
--ion-color-step-650: #acacac;
|
||||
--ion-color-step-700: #b8b8b8;
|
||||
--ion-color-step-750: #c4c4c4;
|
||||
--ion-color-step-800: #d0d0d0;
|
||||
--ion-color-step-850: #dbdbdb;
|
||||
--ion-color-step-900: #e7e7e7;
|
||||
--ion-color-step-950: #f3f3f3;
|
||||
|
||||
--ion-item-background: #1e1e1e;
|
||||
|
||||
--ion-toolbar-background: #1f1f1f;
|
||||
|
||||
--ion-tab-bar-background: #1f1f1f;
|
||||
|
||||
--ion-card-background: #1e1e1e;
|
||||
}
|
||||
/* } */
|
||||
|
||||
.non-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,39 @@
|
||||
import { IonCardSubtitle, IonCardTitle, IonCol, IonInfiniteScroll, IonInfiniteScrollContent, IonList, IonRow } from "@ionic/react";
|
||||
import { connectInfiniteHits } from "react-instantsearch-core";
|
||||
import CustomSearchHit from "./CustomSearchHit";
|
||||
|
||||
const CustomInfiniteHits = ({ hits, hasMore, refineNext }) => {
|
||||
|
||||
const getMore = (e, refine) => {
|
||||
|
||||
refine();
|
||||
e.target.complete();
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<div className="ais-InfiniteHits">
|
||||
<IonList className="ais-InfiniteHits-list">
|
||||
<div className="ais-Hits__root">
|
||||
|
||||
<IonRow>
|
||||
{ hits.map(hit => <CustomSearchHit key={ hit.objectID } hit={ hit } />) }
|
||||
|
||||
{ hits.length < 1 &&
|
||||
<IonCol size="12" className="ion-text-center ion-padding-top ion-margin-top">
|
||||
<IonCardTitle>No results found</IonCardTitle>
|
||||
<IonCardSubtitle>Try something else</IonCardSubtitle>
|
||||
</IonCol>
|
||||
}
|
||||
</IonRow>
|
||||
|
||||
<IonInfiniteScroll threshold="100px" onIonInfinite={ e => getMore(e, refineNext) }>
|
||||
<IonInfiniteScrollContent loadingSpinner="bubbles" loadingText="Getting more movies..." />
|
||||
</IonInfiniteScroll>
|
||||
</div>
|
||||
</IonList>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectInfiniteHits(CustomInfiniteHits);
|
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
IonCardSubtitle,
|
||||
IonCardTitle,
|
||||
IonCol,
|
||||
IonInfiniteScroll,
|
||||
IonInfiniteScrollContent,
|
||||
IonList,
|
||||
IonRow,
|
||||
} from '@ionic/react';
|
||||
import { connectInfiniteHits } from 'react-instantsearch-core';
|
||||
import CustomSearchHit from './CustomSearchHit';
|
||||
|
||||
const CustomInfiniteHits = ({ hits, hasMore, refineNext }): React.JSX.Element => {
|
||||
const getMore = (e, refine) => {
|
||||
refine();
|
||||
e.target.complete();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ais-InfiniteHits">
|
||||
<IonList className="ais-InfiniteHits-list">
|
||||
<div className="ais-Hits__root">
|
||||
<IonRow>
|
||||
{hits.map((hit) => (
|
||||
<CustomSearchHit key={hit.objectID} hit={hit} />
|
||||
))}
|
||||
|
||||
{hits.length < 1 && (
|
||||
<IonCol size="12" className="ion-text-center ion-padding-top ion-margin-top">
|
||||
<IonCardTitle>No results found</IonCardTitle>
|
||||
<IonCardSubtitle>Try something else</IonCardSubtitle>
|
||||
</IonCol>
|
||||
)}
|
||||
</IonRow>
|
||||
|
||||
<IonInfiniteScroll threshold="100px" onIonInfinite={(e) => getMore(e, refineNext)}>
|
||||
<IonInfiniteScrollContent
|
||||
loadingSpinner="bubbles"
|
||||
loadingText="Getting more movies..."
|
||||
/>
|
||||
</IonInfiniteScroll>
|
||||
</div>
|
||||
</IonList>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connectInfiniteHits(CustomInfiniteHits);
|
@@ -0,0 +1,29 @@
|
||||
import { IonCol, IonRouterLink } from "@ionic/react";
|
||||
import { Link } from "react-router-dom";
|
||||
import styles from "../pages/Movies.module.scss";
|
||||
|
||||
const CustomSearchHit = ({ hit }) => {
|
||||
|
||||
// Any movies without an image, lets just exclude them
|
||||
// So the UI looks nicer.
|
||||
// When I scraped the data, I put a placeholder URL image in
|
||||
// But after more thought, it looks cleaner without including these.
|
||||
if (hit && hit.image !== "https://critics.io/img/movies/poster-placeholder.png" && hit.backdrop_path !== null) {
|
||||
return (
|
||||
|
||||
<IonCol size="6" className={ styles.movie }>
|
||||
<Link to={{ pathname: `/movie/${ hit.objectID }`, state: { movie: hit } }} className="non-link">
|
||||
<div className={ styles.movieInfo }>
|
||||
<img src={ hit.image } alt="movie poster" />
|
||||
<h2>{ hit.title }</h2>
|
||||
</div>
|
||||
</Link>
|
||||
</IonCol>
|
||||
);
|
||||
} else {
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomSearchHit;
|
@@ -0,0 +1,33 @@
|
||||
import { IonCol, IonRouterLink } from '@ionic/react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styles from '../pages/Movies.module.scss';
|
||||
|
||||
const CustomSearchHit = ({ hit }): React.JSX.Element | null => {
|
||||
// Any movies without an image, lets just exclude them
|
||||
// So the UI looks nicer.
|
||||
// When I scraped the data, I put a placeholder URL image in
|
||||
// But after more thought, it looks cleaner without including these.
|
||||
if (
|
||||
hit &&
|
||||
hit.image !== 'https://critics.io/img/movies/poster-placeholder.png' &&
|
||||
hit.backdrop_path !== null
|
||||
) {
|
||||
return (
|
||||
<IonCol size="6" className={styles.movie}>
|
||||
<Link
|
||||
to={{ pathname: `/movie/${hit.objectID}`, state: { movie: hit } }}
|
||||
className="non-link"
|
||||
>
|
||||
<div className={styles.movieInfo}>
|
||||
<img src={hit.image} alt="movie poster" />
|
||||
<h2>{hit.title}</h2>
|
||||
</div>
|
||||
</Link>
|
||||
</IonCol>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default CustomSearchHit;
|
@@ -0,0 +1,9 @@
|
||||
import { IonSearchbar } from "@ionic/react";
|
||||
import { searchCircleOutline } from "ionicons/icons";
|
||||
import { connectSearchBox } from "react-instantsearch-core";
|
||||
|
||||
const CustomSearchBox = ({ currentRefinement, refine }) => (
|
||||
<IonSearchbar animated={ true } onIonCancel={ () => refine('') } value={ currentRefinement } onIonChange={ event => refine(event.currentTarget.value) } onKeyUp={ event => refine(event.currentTarget.value) } placeholder="Try 'Avengers'" icon={ searchCircleOutline } slot="end" />
|
||||
);
|
||||
|
||||
export default connectSearchBox(CustomSearchBox);
|
@@ -0,0 +1,18 @@
|
||||
import { IonSearchbar } from '@ionic/react';
|
||||
import { searchCircleOutline } from 'ionicons/icons';
|
||||
import { connectSearchBox } from 'react-instantsearch-core';
|
||||
|
||||
const CustomSearchBox = ({ currentRefinement, refine }): React.JSX.Element => (
|
||||
<IonSearchbar
|
||||
animated={true}
|
||||
onIonCancel={() => refine('')}
|
||||
value={currentRefinement}
|
||||
onIonChange={(event) => refine(event.currentTarget.value)}
|
||||
onKeyUp={(event) => refine(event.currentTarget.value)}
|
||||
placeholder="Try 'Avengers'"
|
||||
icon={searchCircleOutline}
|
||||
slot="end"
|
||||
/>
|
||||
);
|
||||
|
||||
export default connectSearchBox(CustomSearchBox);
|
@@ -6,7 +6,8 @@ import { Route, Redirect } from 'react-router';
|
||||
import Tab1 from './AppPages/Tab1';
|
||||
import Tab2 from './AppPages/Tab2';
|
||||
|
||||
import './style.scss';
|
||||
// import './style.scss';
|
||||
import './theme/variables.scss';
|
||||
|
||||
function DemoReactMovieAppWithAlgolia() {
|
||||
return (
|
||||
@@ -19,7 +20,11 @@ function DemoReactMovieAppWithAlgolia() {
|
||||
<Tab2 />
|
||||
</Route>
|
||||
|
||||
<Redirect exact path="/demo-react-movie-app-with-algolia" to="/demo-react-movie-app-with-algolia/tab1" />
|
||||
<Redirect
|
||||
exact
|
||||
path="/demo-react-movie-app-with-algolia"
|
||||
to="/demo-react-movie-app-with-algolia/tab1"
|
||||
/>
|
||||
</IonRouterOutlet>
|
||||
|
||||
{/* */}
|
||||
|
@@ -0,0 +1,86 @@
|
||||
import { IonBackButton, IonBadge, IonButton, IonCardSubtitle, IonCardTitle, IonCol, IonContent, IonGrid, IonHeader, IonPage, IonRow } from '@ionic/react';
|
||||
import styles from './Movie.module.scss';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { arrowBack } from 'ionicons/icons';
|
||||
import { useLocation, useParams } from 'react-router';
|
||||
|
||||
const Movie = () => {
|
||||
|
||||
const params = useParams();
|
||||
const location = useLocation();
|
||||
const [ movie, setMovie ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (location.state.movie) {
|
||||
|
||||
const backdropPath = `https://image.tmdb.org/t/p/w500${ location.state.movie.backdrop_path }`;
|
||||
location.state.movie.cover_image = backdropPath;
|
||||
|
||||
setMovie(location.state.movie);
|
||||
}
|
||||
}, [ params ]);
|
||||
|
||||
const parseDate = dateToParse => {
|
||||
|
||||
const dateParts = dateToParse.split("-");
|
||||
return `${ dateParts[2] }-${ dateParts[1] }-${ dateParts[0] }`;
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage className={ styles.page }>
|
||||
<IonHeader>
|
||||
<img src={ movie.cover_image } alt="movie cover" />
|
||||
<IonBackButton color="light" className={ styles.backButton } text=" Back to search" icon={ arrowBack } />
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonGrid>
|
||||
<IonRow>
|
||||
<IonCol size="5">
|
||||
<img src={ movie.image } />
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="7" className={ styles.movieInfo }>
|
||||
<IonCardSubtitle>Title</IonCardSubtitle>
|
||||
<IonCardTitle>{ movie.title }</IonCardTitle>
|
||||
|
||||
<IonRow className="ion-justify-content-center ion-margin-top">
|
||||
<IonCol size="6">
|
||||
<IonCardSubtitle>Popularity</IonCardSubtitle>
|
||||
<IonBadge className={ styles.infoBadge }>{ movie.popularity ? movie.popularity : "Unknown" }</IonBadge>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="6">
|
||||
<IonCardSubtitle>Release Date</IonCardSubtitle>
|
||||
<IonBadge className={ styles.infoBadge }>{ movie.release_date ? parseDate(movie.release_date) : "Unknown" }</IonBadge>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<IonCardSubtitle>Search for Youtube Trailer</IonCardSubtitle>
|
||||
<a href={ `https://www.youtube.com/results?search_query=${ movie.title } trailer` } rel="noopener" target="_new">
|
||||
<IonButton>
|
||||
Search now
|
||||
</IonButton>
|
||||
</a>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow className={ styles.movieOverview }>
|
||||
<IonCol size="12">
|
||||
<IonCardSubtitle>Overview</IonCardSubtitle>
|
||||
<p>{ movie.overview ? movie.overview : `No overview to show unfortunately. Try doing a google search for '${ movie.title }.` }</p>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Movie;
|
@@ -0,0 +1,54 @@
|
||||
.page {
|
||||
|
||||
ion-header {
|
||||
|
||||
background-color: #373b5e;
|
||||
}
|
||||
|
||||
ion-toolbar {
|
||||
|
||||
--border-style: none;
|
||||
--background: #373b5e;
|
||||
--color: white;
|
||||
--min-height: 8rem;
|
||||
--stripe: #373b5e;
|
||||
--bg: #3e4368;
|
||||
--background: transparent;
|
||||
background: linear-gradient(135deg, var(--bg) 25%, transparent 25%) -50px 0,
|
||||
linear-gradient(225deg, var(--bg) 25%, transparent 25%) -50px 0,
|
||||
linear-gradient(315deg, var(--bg) 25%, transparent 25%),
|
||||
linear-gradient(45deg, var(--bg) 25%, transparent 25%);
|
||||
background-size: 100px 100px;
|
||||
background-color: var(--stripe);
|
||||
|
||||
ion-title {
|
||||
|
||||
--color: white;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.movieOverview {
|
||||
|
||||
background-color:#373b5e;
|
||||
padding: 2rem;
|
||||
color: white;
|
||||
|
||||
ion-card-subtitle {
|
||||
|
||||
--color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.backButton {
|
||||
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.infoBadge {
|
||||
|
||||
background-color:rgba(44, 53, 105, 0.8);
|
||||
}
|
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
IonBackButton,
|
||||
IonBadge,
|
||||
IonButton,
|
||||
IonCardSubtitle,
|
||||
IonCardTitle,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonGrid,
|
||||
IonHeader,
|
||||
IonPage,
|
||||
IonRow,
|
||||
} from '@ionic/react';
|
||||
import styles from './Movie.module.scss';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { arrowBack } from 'ionicons/icons';
|
||||
import { useLocation, useParams } from 'react-router';
|
||||
|
||||
const Movie = (): React.JSX.Element => {
|
||||
const params = useParams();
|
||||
const location = useLocation();
|
||||
const [movie, setMovie] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (location.state.movie) {
|
||||
const backdropPath = `https://image.tmdb.org/t/p/w500${location.state.movie.backdrop_path}`;
|
||||
location.state.movie.cover_image = backdropPath;
|
||||
|
||||
setMovie(location.state.movie);
|
||||
}
|
||||
}, [params]);
|
||||
|
||||
const parseDate = (dateToParse) => {
|
||||
const dateParts = dateToParse.split('-');
|
||||
return `${dateParts[2]}-${dateParts[1]}-${dateParts[0]}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage className={styles.page}>
|
||||
<IonHeader>
|
||||
<img src={movie.cover_image} alt="movie cover" />
|
||||
<IonBackButton
|
||||
color="light"
|
||||
className={styles.backButton}
|
||||
text=" Back to search"
|
||||
icon={arrowBack}
|
||||
/>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonGrid>
|
||||
<IonRow>
|
||||
<IonCol size="5">
|
||||
<img src={movie.image} />
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="7" className={styles.movieInfo}>
|
||||
<IonCardSubtitle>Title</IonCardSubtitle>
|
||||
<IonCardTitle>{movie.title}</IonCardTitle>
|
||||
|
||||
<IonRow className="ion-justify-content-center ion-margin-top">
|
||||
<IonCol size="6">
|
||||
<IonCardSubtitle>Popularity</IonCardSubtitle>
|
||||
<IonBadge className={styles.infoBadge}>
|
||||
{movie.popularity ? movie.popularity : 'Unknown'}
|
||||
</IonBadge>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="6">
|
||||
<IonCardSubtitle>Release Date</IonCardSubtitle>
|
||||
<IonBadge className={styles.infoBadge}>
|
||||
{movie.release_date ? parseDate(movie.release_date) : 'Unknown'}
|
||||
</IonBadge>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<IonCardSubtitle>Search for Youtube Trailer</IonCardSubtitle>
|
||||
<a
|
||||
href={`https://www.youtube.com/results?search_query=${movie.title} trailer`}
|
||||
rel="noopener"
|
||||
target="_new"
|
||||
>
|
||||
<IonButton>Search now</IonButton>
|
||||
</a>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow className={styles.movieOverview}>
|
||||
<IonCol size="12">
|
||||
<IonCardSubtitle>Overview</IonCardSubtitle>
|
||||
<p>
|
||||
{movie.overview
|
||||
? movie.overview
|
||||
: `No overview to show unfortunately. Try doing a google search for '${movie.title}.`}
|
||||
</p>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Movie;
|
@@ -0,0 +1,94 @@
|
||||
import { IonButton, IonCardSubtitle, IonCardTitle, IonContent, IonHeader, IonIcon, IonPage, IonToolbar } from '@ionic/react';
|
||||
import styles from './Movies.module.scss';
|
||||
|
||||
// Algolia imports
|
||||
import algoliasearch from 'algoliasearch/lite';
|
||||
import { InstantSearch } from 'react-instantsearch-dom';
|
||||
|
||||
// Custom Algolia UI
|
||||
import CustomSearchbox from '../components/CustomSearchbox';
|
||||
import CustomInfiniteHits from '../components/CustomInfiniteHits';
|
||||
import { useEffect } from 'react';
|
||||
import { addOutline } from 'ionicons/icons';
|
||||
|
||||
const searchClient = algoliasearch('QZKBD6VPU7', 'db21b77f5f3bf4d4cbde385b7f33c60d');
|
||||
|
||||
const Movies = props => {
|
||||
|
||||
// PWA functionality for a custom add to homescreen
|
||||
// This allows us to display a custom button based on service worker
|
||||
useEffect(() => {
|
||||
|
||||
const buttInstall = document.getElementById('buttInstall');
|
||||
window.addEventListener('beforeinstallprompt', (event) => {
|
||||
|
||||
console.log('👍', 'beforeinstallprompt', event);
|
||||
|
||||
// Save the event so it can be triggered later.
|
||||
window.deferredPrompt = event;
|
||||
buttInstall.classList.toggle('hidden', false);
|
||||
});
|
||||
|
||||
window.addEventListener('appinstalled', (event) => {
|
||||
|
||||
console.log('👍', 'appinstalled', event);
|
||||
// Clear the deferredPrompt so it can be garbage collected
|
||||
window.deferredPrompt = null;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const addToHomeScreen = async () => {
|
||||
|
||||
const buttInstall = document.getElementById('buttInstall');
|
||||
|
||||
console.log('👍', 'buttInstall-clicked');
|
||||
const promptEvent = window.deferredPrompt;
|
||||
|
||||
if (!promptEvent) {
|
||||
|
||||
// The deferred prompt isn't available.
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the install prompt.
|
||||
promptEvent.prompt();
|
||||
|
||||
// Log the result
|
||||
const result = await promptEvent.userChoice;
|
||||
console.log('👍', 'userChoice', result);
|
||||
|
||||
// Reset the deferred prompt variable, since
|
||||
// prompt() can only be called once.
|
||||
window.deferredPrompt = null;
|
||||
|
||||
// Hide the install button.
|
||||
buttInstall.classList.toggle('hidden', true);
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage className={ styles.page }>
|
||||
|
||||
<InstantSearch searchClient={ searchClient } indexName="dev_movies">
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<div className={ styles.searchContainer }>
|
||||
<IonCardTitle>Movie List</IonCardTitle>
|
||||
<IonCardSubtitle>with algolia search</IonCardSubtitle>
|
||||
<CustomSearchbox />
|
||||
|
||||
<IonButton id="buttInstall" color="success" fill="solid" size="small" className="hidden add-button" onClick={ () => addToHomeScreen() }>
|
||||
<IonIcon icon={ addOutline } /> Install App
|
||||
</IonButton>
|
||||
</div>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<CustomInfiniteHits />
|
||||
</IonContent>
|
||||
</InstantSearch>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Movies;
|
@@ -0,0 +1,133 @@
|
||||
.page {
|
||||
|
||||
ion-header {
|
||||
|
||||
background-color: #373b5e;
|
||||
}
|
||||
|
||||
ion-toolbar {
|
||||
|
||||
--border-style: none;
|
||||
--background: #373b5e;
|
||||
--color: white;
|
||||
--min-height: 8rem;
|
||||
--stripe: #373b5e;
|
||||
--bg: #3e4368;
|
||||
--background: transparent;
|
||||
background: linear-gradient(135deg, var(--bg) 25%, transparent 25%) -50px 0,
|
||||
linear-gradient(225deg, var(--bg) 25%, transparent 25%) -50px 0,
|
||||
linear-gradient(315deg, var(--bg) 25%, transparent 25%),
|
||||
linear-gradient(45deg, var(--bg) 25%, transparent 25%);
|
||||
background-size: 100px 100px;
|
||||
background-color: var(--stripe);
|
||||
|
||||
ion-button {
|
||||
|
||||
font-size: 1.2rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.searchContainer {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: white;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
|
||||
ion-card-title,
|
||||
ion-card-subtitle {
|
||||
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ion-card-title {
|
||||
|
||||
font-size: 1.5rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
ion-card-subtitle {
|
||||
|
||||
color: rgb(189, 189, 189);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ion-searchbar {
|
||||
|
||||
--border-radius: 10px;
|
||||
--background: white;
|
||||
--color: black;
|
||||
padding: 0.5rem !important;
|
||||
margin: 0 !important;
|
||||
--background: #2a2d44;
|
||||
--color: white;
|
||||
--icon-color: white;
|
||||
--clear-button-color: white;
|
||||
}
|
||||
|
||||
ion-searchbar {
|
||||
|
||||
input{
|
||||
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
ion-list {
|
||||
|
||||
background-color: #e7edfb;
|
||||
}
|
||||
}
|
||||
|
||||
.movie {
|
||||
|
||||
.movieInfo {
|
||||
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
|
||||
img {
|
||||
|
||||
height: 75vmin;
|
||||
}
|
||||
|
||||
h2 {
|
||||
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
background-color: red;
|
||||
width: 95%;
|
||||
background-color: rgba(44, 53, 105, 0.8);
|
||||
color: white;
|
||||
font-size: 4vmin;
|
||||
padding: 1rem;
|
||||
min-height: 4.5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
max-height: 4.5rem;
|
||||
margin-bottom: 0;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ion-button {
|
||||
|
||||
--background: #5a55ca;
|
||||
--background-focused: #6f6bbb;
|
||||
--background-activated: #6f6bbb;
|
||||
--padding-top: 1rem;
|
||||
--padding-bottom: 1rem;
|
||||
--padding-start: 0.75rem;
|
||||
--padding-end: 0.75rem;
|
||||
margin-top: -0.2rem;
|
||||
}
|
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonCardSubtitle,
|
||||
IonCardTitle,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonPage,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
import styles from './Movies.module.scss';
|
||||
|
||||
// Algolia imports
|
||||
import algoliasearch from 'algoliasearch/lite';
|
||||
import { InstantSearch } from 'react-instantsearch-dom';
|
||||
|
||||
// Custom Algolia UI
|
||||
import CustomSearchbox from '../components/CustomSearchbox';
|
||||
import CustomInfiniteHits from '../components/CustomInfiniteHits';
|
||||
import { useEffect } from 'react';
|
||||
import { addOutline } from 'ionicons/icons';
|
||||
|
||||
const searchClient = algoliasearch('QZKBD6VPU7', 'db21b77f5f3bf4d4cbde385b7f33c60d');
|
||||
|
||||
const Movies = (props: any): React.JSX.Element => {
|
||||
// PWA functionality for a custom add to homescreen
|
||||
// This allows us to display a custom button based on service worker
|
||||
useEffect(() => {
|
||||
const buttInstall = document.getElementById('buttInstall');
|
||||
window.addEventListener('beforeinstallprompt', (event) => {
|
||||
console.log('👍', 'beforeinstallprompt', event);
|
||||
|
||||
// Save the event so it can be triggered later.
|
||||
window.deferredPrompt = event;
|
||||
buttInstall.classList.toggle('hidden', false);
|
||||
});
|
||||
|
||||
window.addEventListener('appinstalled', (event) => {
|
||||
console.log('👍', 'appinstalled', event);
|
||||
// Clear the deferredPrompt so it can be garbage collected
|
||||
window.deferredPrompt = null;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const addToHomeScreen = async () => {
|
||||
const buttInstall = document.getElementById('buttInstall');
|
||||
|
||||
console.log('👍', 'buttInstall-clicked');
|
||||
const promptEvent = window.deferredPrompt;
|
||||
|
||||
if (!promptEvent) {
|
||||
// The deferred prompt isn't available.
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the install prompt.
|
||||
promptEvent.prompt();
|
||||
|
||||
// Log the result
|
||||
const result = await promptEvent.userChoice;
|
||||
console.log('👍', 'userChoice', result);
|
||||
|
||||
// Reset the deferred prompt variable, since
|
||||
// prompt() can only be called once.
|
||||
window.deferredPrompt = null;
|
||||
|
||||
// Hide the install button.
|
||||
buttInstall.classList.toggle('hidden', true);
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage className={styles.page}>
|
||||
<InstantSearch searchClient={searchClient} indexName="dev_movies">
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<div className={styles.searchContainer}>
|
||||
<IonCardTitle>Movie List</IonCardTitle>
|
||||
<IonCardSubtitle>with algolia search</IonCardSubtitle>
|
||||
<CustomSearchbox />
|
||||
|
||||
<IonButton
|
||||
id="buttInstall"
|
||||
color="success"
|
||||
fill="solid"
|
||||
size="small"
|
||||
className="hidden add-button"
|
||||
onClick={() => addToHomeScreen()}
|
||||
>
|
||||
<IonIcon icon={addOutline} />
|
||||
Install App
|
||||
</IonButton>
|
||||
</div>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<CustomInfiniteHits />
|
||||
</IonContent>
|
||||
</InstantSearch>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Movies;
|
@@ -1,103 +0,0 @@
|
||||
#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,24 @@
|
||||
.container {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.container strong {
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.container p {
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
color: #8c8c8c;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.container a {
|
||||
text-decoration: none;
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
import './ExploreContainer.css';
|
||||
|
||||
const ExploreContainer = (): React.JSX.Element => {
|
||||
return (
|
||||
<div className="container">
|
||||
<strong>Ready to create an app?</strong>
|
||||
<p>
|
||||
Start with Ionic{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://ionicframework.com/docs/components"
|
||||
>
|
||||
UI Components
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreContainer;
|
@@ -1,38 +1,27 @@
|
||||
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
|
||||
import { IonRouterOutlet, 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';
|
||||
// import Tab1 from './AppPages/Tab1';
|
||||
// import Tab2 from './AppPages/Tab2';
|
||||
|
||||
import './style.scss';
|
||||
import './theme/variables.scss';
|
||||
import Home from './pages/Home';
|
||||
import Add from './pages/Add';
|
||||
|
||||
function DemoReactNotes() {
|
||||
return (
|
||||
<IonTabs>
|
||||
<IonTabs className="demo-react-notes">
|
||||
<IonRouterOutlet>
|
||||
<Route exact path="/demo-react-notes/tab1">
|
||||
<Tab1 />
|
||||
<Route exact path="/demo-react-notes/home">
|
||||
<Home />
|
||||
</Route>
|
||||
<Route exact path="/demo-react-notes/tab2">
|
||||
<Tab2 />
|
||||
<Route exact path="/demo-react-notes/add">
|
||||
<Add />
|
||||
</Route>
|
||||
|
||||
<Redirect exact path="/demo-react-notes" to="/demo-react-notes/tab1" />
|
||||
<Redirect exact path="/demo-react-notes" to="/demo-react-notes/home" />
|
||||
</IonRouterOutlet>
|
||||
|
||||
{/* */}
|
||||
<IonTabBar slot="bottom">
|
||||
<IonTabButton tab="tab1" href="/demo-react-notes/tab1">
|
||||
<IonIcon icon={cloudOutline} />
|
||||
<IonLabel>Dashboard</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="tab2" href="/demo-react-notes/tab2">
|
||||
<IonIcon icon={searchOutline} />
|
||||
<IonLabel>Search</IonLabel>
|
||||
</IonTabButton>
|
||||
</IonTabBar>
|
||||
</IonTabs>
|
||||
);
|
||||
}
|
||||
|
@@ -0,0 +1,29 @@
|
||||
.title {
|
||||
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
|
||||
.customInput {
|
||||
|
||||
border-radius: 22px !important;
|
||||
--padding-bottom: 1rem;
|
||||
--padding-top: 1rem;
|
||||
|
||||
box-shadow: 0 0.7px 0.9px rgba(0, 0, 0, 0.101),
|
||||
0 0.9px 2.5px rgba(0, 0, 0, 0.145),
|
||||
0 2.5px 6px rgba(0, 0, 0, 0.189),
|
||||
0 8px 20px rgba(0, 0, 0, 0.29);
|
||||
}
|
||||
|
||||
.customInput {
|
||||
|
||||
margin-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
.saveButton {
|
||||
|
||||
--border-radius: 12px !important;
|
||||
|
||||
--padding-top: 1.75rem !important;
|
||||
--padding-bottom: 1.75rem !important;
|
||||
}
|
115
03_source/mobile/src/pages/DemoReactNotes/pages/Add.tsx
Normal file
115
03_source/mobile/src/pages/DemoReactNotes/pages/Add.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
IonBackButton,
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonGrid,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonSelect,
|
||||
IonSelectOption,
|
||||
IonTextarea,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonRouter,
|
||||
} from '@ionic/react';
|
||||
import styles from './Add.module.scss';
|
||||
|
||||
import { checkmarkOutline } from 'ionicons/icons';
|
||||
import { getCategories, getNotes } from '../store/Selectors';
|
||||
import { CategoryStore, NoteStore } from '../store';
|
||||
import { addNote } from '../store/NoteStore';
|
||||
import { useState } from 'react';
|
||||
|
||||
const Add = (): React.JSX.Element => {
|
||||
const categories = CategoryStore.useState(getCategories);
|
||||
const notes = NoteStore.useState(getNotes);
|
||||
const [noteCategory, setNoteCategory] = useState<number | false>(false);
|
||||
const [noteContent, setNoteContent] = useState<string>('');
|
||||
const router = useIonRouter();
|
||||
|
||||
const add = () => {
|
||||
const note = {
|
||||
id: notes.length + 1,
|
||||
category_id: noteCategory,
|
||||
note: noteContent,
|
||||
complete: false,
|
||||
};
|
||||
|
||||
addNote(note);
|
||||
router.goBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonBackButton />
|
||||
</IonButtons>
|
||||
|
||||
<IonTitle className={styles.title}>Add note</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonGrid className="ion-padding-top">
|
||||
<IonRow>
|
||||
<IonCol size="12" className="ion-padding-start">
|
||||
<h1 className={styles.mainTitle}>Add a note</h1>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<IonItem lines="none" className={styles.customInput}>
|
||||
<IonLabel position="floating">Category</IonLabel>
|
||||
<IonSelect
|
||||
placeholder="Select..."
|
||||
value={noteCategory}
|
||||
onIonChange={(e) => setNoteCategory(e.target.value)}
|
||||
>
|
||||
{categories.map((category) => {
|
||||
return (
|
||||
<IonSelectOption value={category.id} key={category.id}>
|
||||
{category.name}
|
||||
</IonSelectOption>
|
||||
);
|
||||
})}
|
||||
</IonSelect>
|
||||
</IonItem>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<IonItem lines="none" className={styles.customInput}>
|
||||
<IonLabel position="floating">Note</IonLabel>
|
||||
<IonTextarea
|
||||
value={noteContent}
|
||||
onIonChange={(e) => setNoteContent(e.target.value)}
|
||||
placeholder="Enter note text here..."
|
||||
/>
|
||||
</IonItem>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<IonButton expand="block" className={styles.saveButton} onClick={add}>
|
||||
<IonIcon icon={checkmarkOutline} />
|
||||
Save note
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Add;
|
130
03_source/mobile/src/pages/DemoReactNotes/pages/Home.module.scss
Normal file
130
03_source/mobile/src/pages/DemoReactNotes/pages/Home.module.scss
Normal file
@@ -0,0 +1,130 @@
|
||||
.heading {
|
||||
|
||||
color: var(--lighter-blue-color);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.mainTitle {
|
||||
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.slideHeader {
|
||||
|
||||
margin: 0 !important;
|
||||
padding: 0;
|
||||
padding: 1.5rem !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.slideCount {
|
||||
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
padding-left: 1.5rem !important;
|
||||
padding-top: 1rem !important;
|
||||
}
|
||||
|
||||
.slideCount h6 {
|
||||
|
||||
color: var(--light-blue-color);
|
||||
}
|
||||
|
||||
.slideHeader h4 {
|
||||
|
||||
color: white;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
text-align: left;
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.categorySlider {
|
||||
|
||||
margin-top: -1.6rem;
|
||||
|
||||
ion-slide {
|
||||
|
||||
width: 60%;
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
ion-col {
|
||||
|
||||
padding-left: 0;
|
||||
|
||||
ion-card {
|
||||
|
||||
width: 100%;
|
||||
border-radius: 22px;
|
||||
// box-shadow: 0px 4px 12px 2px rgba(0, 0, 0, 0.16);
|
||||
|
||||
box-shadow: 0 0.7px 0.9px rgba(0, 0, 0, 0.101),
|
||||
0 1.9px 2.5px rgba(0, 0, 0, 0.145),
|
||||
0 4.5px 6px rgba(0, 0, 0, 0.189),
|
||||
0 15px 20px rgba(0, 0, 0, 0.29);
|
||||
|
||||
ion-card-header {
|
||||
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
ion-card-content {
|
||||
|
||||
padding: 0;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lastUsed {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-content: flex-start;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.9rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.categoryColor {
|
||||
|
||||
border-bottom: 3px solid var(--pink-color);
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: -1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.recentNotes {
|
||||
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
max-height: 18.8rem;
|
||||
overflow: scroll;
|
||||
|
||||
ion-item {
|
||||
|
||||
--padding-start: 2rem;
|
||||
--padding-end: 2rem;
|
||||
--border-style: none;
|
||||
--border-radius: 22px;
|
||||
--min-height: 4rem;
|
||||
|
||||
h4 {
|
||||
|
||||
padding-left: 1rem;
|
||||
margin-top: 0.6rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottomContainer {
|
||||
|
||||
margin-top: -1rem;
|
||||
}
|
166
03_source/mobile/src/pages/DemoReactNotes/pages/Home.tsx
Normal file
166
03_source/mobile/src/pages/DemoReactNotes/pages/Home.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonCard,
|
||||
IonCardContent,
|
||||
IonCardHeader,
|
||||
IonCardSubtitle,
|
||||
IonCheckbox,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonFab,
|
||||
IonFabButton,
|
||||
IonGrid,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonItem,
|
||||
IonPage,
|
||||
IonRow,
|
||||
// IonSlide,
|
||||
// IonSlides,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
import styles from './Home.module.scss';
|
||||
|
||||
import { addOutline, menuOutline, notificationsOutline, searchOutline } from 'ionicons/icons';
|
||||
import { getCategories, getNotes } from '../store/Selectors';
|
||||
import { CategoryStore, NoteStore } from '../store';
|
||||
import { markNote } from '../store/NoteStore';
|
||||
|
||||
const Home = (): React.JSX.Element => {
|
||||
return <>TODO: need update IonSlide</>;
|
||||
|
||||
const categories = CategoryStore.useState(getCategories);
|
||||
const notes = NoteStore.useState(getNotes);
|
||||
|
||||
const getNoteStyle = (categoryID, isComplete = false) => {
|
||||
const categoryColor = categories.filter((category) => category.id === categoryID)[0].color;
|
||||
|
||||
return {
|
||||
'--background': categoryColor,
|
||||
'--background-checked': categoryColor,
|
||||
'--border-style': 'none',
|
||||
opacity: isComplete ? '0.6' : '1',
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonButton>
|
||||
<IonIcon icon={menuOutline} />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
|
||||
<IonButtons slot="end">
|
||||
<IonButton>
|
||||
<IonIcon icon={searchOutline} />
|
||||
</IonButton>
|
||||
|
||||
<IonButton>
|
||||
<IonIcon icon={notificationsOutline} />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonGrid>
|
||||
<IonRow>
|
||||
<IonCol size="12" className="ion-padding-start">
|
||||
<h1 className={styles.mainTitle}>Hello, Alan!</h1>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="12" className="ion-padding-start ion-padding-top">
|
||||
<IonCardSubtitle className={styles.heading}>Categories</IonCardSubtitle>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
|
||||
<IonSlides
|
||||
id="slider"
|
||||
options={{ slidesPerView: 'auto', zoom: true, grabCursor: true }}
|
||||
className={`${styles.categorySlider} ion-padding-bottom`}
|
||||
>
|
||||
{categories.map((category, index) => {
|
||||
const noteCount = notes.filter((n) => n.category_id === category.id).length;
|
||||
|
||||
return (
|
||||
<IonSlide key={`categorySlide_${index}`}>
|
||||
<IonCol className="ion-text-left">
|
||||
<IonCard>
|
||||
<IonCardHeader className="ion-no-padding">
|
||||
<div className={styles.slideCount}>
|
||||
<h6>
|
||||
{noteCount} {noteCount === 1 ? 'note' : 'notes'}{' '}
|
||||
</h6>
|
||||
</div>
|
||||
<div className={styles.slideHeader}>
|
||||
<h4>{category.name}</h4>
|
||||
</div>
|
||||
</IonCardHeader>
|
||||
|
||||
<IonCardContent>
|
||||
<div
|
||||
className={styles.categoryColor}
|
||||
style={{ borderBottom: `2px solid ${category.color}` }}
|
||||
></div>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
</IonCol>
|
||||
</IonSlide>
|
||||
);
|
||||
})}
|
||||
</IonSlides>
|
||||
|
||||
<IonGrid className={styles.bottomContainer}>
|
||||
<IonRow>
|
||||
<IonCol size="12" className="ion-padding-start">
|
||||
<IonCardSubtitle className={styles.heading}>Recent Notes</IonCardSubtitle>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<div className={styles.recentNotes}>
|
||||
{notes.map((note, index) => {
|
||||
return (
|
||||
<IonRow
|
||||
key={`note_${index}`}
|
||||
className="animate__animated animate__faster"
|
||||
id={`noteRow_${note.id}`}
|
||||
>
|
||||
<IonCol size="12">
|
||||
<IonItem>
|
||||
<IonCheckbox
|
||||
checked={note.complete}
|
||||
style={getNoteStyle(note.category_id, note.complete)}
|
||||
onClick={() => markNote(note.id)}
|
||||
/>
|
||||
<h4
|
||||
style={
|
||||
note.complete ? { textDecoration: 'line-through', opacity: '0.6' } : {}
|
||||
}
|
||||
>
|
||||
{note.note}
|
||||
</h4>
|
||||
</IonItem>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</IonGrid>
|
||||
|
||||
<IonFab vertical="bottom" horizontal="end" slot="fixed" className="ion-padding">
|
||||
<IonFabButton routerLink="/add">
|
||||
<IonIcon icon={addOutline} />
|
||||
</IonFabButton>
|
||||
</IonFab>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
@@ -0,0 +1,33 @@
|
||||
import { Store } from 'pullstate';
|
||||
|
||||
interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
count: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const CategoryStore = new Store({
|
||||
categories: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Business',
|
||||
count: '34',
|
||||
color: '#60b660',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Personal',
|
||||
count: '12',
|
||||
color: '#1D68DF',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Leisure',
|
||||
count: '23',
|
||||
color: '#EB06FF',
|
||||
},
|
||||
] as Category[],
|
||||
});
|
||||
|
||||
export default CategoryStore;
|
58
03_source/mobile/src/pages/DemoReactNotes/store/NoteStore.ts
Normal file
58
03_source/mobile/src/pages/DemoReactNotes/store/NoteStore.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Store } from 'pullstate';
|
||||
|
||||
interface Note {
|
||||
id: number;
|
||||
category_id: number;
|
||||
note: string;
|
||||
complete: boolean;
|
||||
}
|
||||
|
||||
const NoteStore = new Store({
|
||||
notes: [
|
||||
{
|
||||
id: 1,
|
||||
category_id: 1,
|
||||
note: 'Daily meeting with team',
|
||||
complete: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category_id: 2,
|
||||
note: 'Pay monthly rent',
|
||||
complete: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
category_id: 3,
|
||||
note: 'Workout in the gym',
|
||||
complete: false,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
category_id: 1,
|
||||
note: 'Make progress on project',
|
||||
complete: false,
|
||||
},
|
||||
] as Note[],
|
||||
});
|
||||
|
||||
export const markNote = (noteID: number) => {
|
||||
const noteIndex = NoteStore.currentState.notes.findIndex((n) => n.id === noteID);
|
||||
NoteStore.update((s) => {
|
||||
s.notes[noteIndex].complete = !s.notes[noteIndex].complete;
|
||||
});
|
||||
|
||||
document.getElementById(`noteRow_${noteID}`).classList.add('animate__pulse');
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById(`noteRow_${noteID}`).classList.remove('animate__pulse');
|
||||
}, 500);
|
||||
};
|
||||
|
||||
export const addNote = (note: Note) => {
|
||||
NoteStore.update((s) => {
|
||||
s.notes = [note, ...s.notes];
|
||||
});
|
||||
};
|
||||
|
||||
export default NoteStore;
|
16
03_source/mobile/src/pages/DemoReactNotes/store/Selectors.ts
Normal file
16
03_source/mobile/src/pages/DemoReactNotes/store/Selectors.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import { Category, Note } from './NoteStore';
|
||||
|
||||
interface State {
|
||||
categories: Category[];
|
||||
notes: Note[];
|
||||
}
|
||||
|
||||
const getState = (state: State) => state;
|
||||
|
||||
// General getters
|
||||
export const getCategories = createSelector(getState, (state) => state.categories);
|
||||
export const getNotes = createSelector(getState, (state) => state.notes);
|
||||
|
||||
// More specific getters
|
||||
// export const getCoffee = (id: number) => createSelector(getState, state => state.coffees.filter(c => parseInt(c.id) === parseInt(id))[0]);
|
2
03_source/mobile/src/pages/DemoReactNotes/store/index.js
Normal file
2
03_source/mobile/src/pages/DemoReactNotes/store/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as CategoryStore } from "./CategoryStore";
|
||||
export { default as NoteStore } from "./NoteStore";
|
@@ -1,103 +0,0 @@
|
||||
#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,59 @@
|
||||
import { IonSlide, IonButton, IonGrid, IonRow, IonCol } from '@ionic/react';
|
||||
import '../pages/Home.css';
|
||||
|
||||
interface OnboardingSlideProps {
|
||||
image: string;
|
||||
mainSlide?: boolean;
|
||||
finalSlide?: boolean;
|
||||
title: string;
|
||||
text: string;
|
||||
lastSlide?: boolean;
|
||||
sliderRef: React.RefObject<any>;
|
||||
}
|
||||
|
||||
const OnboardingSlide = ({
|
||||
image,
|
||||
mainSlide = false,
|
||||
finalSlide = false,
|
||||
title,
|
||||
text,
|
||||
lastSlide,
|
||||
sliderRef,
|
||||
}: OnboardingSlideProps): React.JSX.Element => {
|
||||
return (
|
||||
<IonSlide>
|
||||
<IonGrid className="ion-justify-content-center ion-align-items-center ion-align-self-center">
|
||||
<IonRow className="slide-content-container">
|
||||
<IonCol size="10" className="slide-content">
|
||||
<img src={image} className={mainSlide && 'slide-main-image'} />
|
||||
<h1>{title}</h1>
|
||||
<p>{text}</p>
|
||||
|
||||
{mainSlide && (
|
||||
<IonButton
|
||||
expand="block"
|
||||
fill="outline"
|
||||
onClick={() => sliderRef.current.slideNext()}
|
||||
>
|
||||
Get started →
|
||||
</IonButton>
|
||||
)}
|
||||
|
||||
{finalSlide && (
|
||||
<>
|
||||
<IonButton expand="block" fill="solid">
|
||||
Register
|
||||
</IonButton>
|
||||
<IonButton expand="block" fill="outline">
|
||||
Login
|
||||
</IonButton>
|
||||
</>
|
||||
)}
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonSlide>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingSlide;
|
@@ -0,0 +1,52 @@
|
||||
ion-slides {
|
||||
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.slide-grid {
|
||||
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.slide-main-image {
|
||||
|
||||
height: 5rem !important;
|
||||
}
|
||||
|
||||
.slide-buttons {
|
||||
|
||||
position: absolute;
|
||||
bottom: 2rem;
|
||||
z-index: 10;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.slide-content-container {
|
||||
|
||||
margin-top: -4rem;
|
||||
}
|
||||
|
||||
.slide-content {
|
||||
|
||||
margin: 0 auto;
|
||||
/* margin-top: 5rem; */
|
||||
color: var(--ion-color-primary);
|
||||
/* background-color: var(--ion-color-primary); */
|
||||
/* color: white; */
|
||||
border: 2px solid rgb(228, 228, 228);
|
||||
border-radius: 15px;
|
||||
padding: 3rem;
|
||||
/* padding-left: 3rem; */
|
||||
/* padding-right: 3rem; */
|
||||
}
|
||||
|
||||
.slide-content p {
|
||||
|
||||
color: rgb(161, 161, 161);
|
||||
}
|
@@ -0,0 +1,89 @@
|
||||
import { IonButton, IonContent, IonIcon, IonPage, IonRow, IonSlides } from '@ionic/react';
|
||||
import { arrowBack, arrowForward } from 'ionicons/icons';
|
||||
import { useRef, useState } from 'react';
|
||||
import OnboardingSlide from '../components/OnboardingSlide';
|
||||
import './Home.css';
|
||||
|
||||
interface SlideContent {
|
||||
image: string;
|
||||
mainSlide?: boolean;
|
||||
finalSlide?: boolean;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const Home = (): React.JSX.Element => {
|
||||
const sliderRef = useRef<HTMLIonSlidesElement>(null);
|
||||
const [lastSlide, setLastSlide] = useState(false);
|
||||
const [firstSlide, setFirstSlide] = useState(true);
|
||||
|
||||
const slideContent: SlideContent[] = [
|
||||
{
|
||||
image: '/assets/applogo1.png',
|
||||
mainSlide: true,
|
||||
title: 'Ionic Onboarding UI',
|
||||
text: 'Share moments with your followers and experience memorable captures',
|
||||
},
|
||||
{
|
||||
image: '/assets/1sub.png',
|
||||
title: 'Capture',
|
||||
text: 'Capture that perfect moment in your life',
|
||||
},
|
||||
{
|
||||
image: '/assets/2sub.png',
|
||||
title: 'Organize',
|
||||
text: 'Organize photos exactly how you want them',
|
||||
},
|
||||
{
|
||||
image: '/assets/3sub.png',
|
||||
title: 'Share',
|
||||
finalSlide: true,
|
||||
text: 'Are you ready to share your special moments online with the world?',
|
||||
},
|
||||
];
|
||||
|
||||
const checkSlides = async () => {
|
||||
if (!sliderRef.current) return;
|
||||
|
||||
const isLastSlide = await sliderRef.current.isEnd();
|
||||
const isFirstSlide = await sliderRef.current.isBeginning();
|
||||
setLastSlide(isLastSlide);
|
||||
setFirstSlide(isFirstSlide);
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonContent fullscreen>
|
||||
<IonSlides
|
||||
onIonSlideWillChange={checkSlides}
|
||||
pager={true}
|
||||
ref={sliderRef}
|
||||
id="slider"
|
||||
options={{ slidesPerView: 'auto', zoom: true, grabCursor: true }}
|
||||
>
|
||||
{slideContent.map((slide, index) => {
|
||||
return (
|
||||
<OnboardingSlide key={index} {...slide} lastSlide={lastSlide} sliderRef={sliderRef} />
|
||||
);
|
||||
})}
|
||||
</IonSlides>
|
||||
|
||||
<IonRow className="slide-buttons">
|
||||
{!firstSlide && (
|
||||
<IonButton fill="clear" onClick={() => sliderRef.current?.slidePrev()}>
|
||||
<IonIcon icon={arrowBack} />
|
||||
</IonButton>
|
||||
)}
|
||||
|
||||
{!lastSlide && (
|
||||
<IonButton fill="clear" onClick={() => sliderRef.current?.slideNext()}>
|
||||
<IonIcon icon={arrowForward} />
|
||||
</IonButton>
|
||||
)}
|
||||
</IonRow>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
@@ -0,0 +1,82 @@
|
||||
/* Ionic Variables and Theming. For more info, please see:
|
||||
http://ionicframework.com/docs/theming/ */
|
||||
|
||||
/** Ionic CSS Variables **/
|
||||
:root {
|
||||
/** primary **/
|
||||
--ion-color-primary: #3880ff;
|
||||
--ion-color-primary-rgb: 56, 128, 255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #3171e0;
|
||||
--ion-color-primary-tint: #4c8dff;
|
||||
|
||||
/** secondary **/
|
||||
--ion-color-secondary: #3dc2ff;
|
||||
--ion-color-secondary-rgb: 61, 194, 255;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #36abe0;
|
||||
--ion-color-secondary-tint: #50c8ff;
|
||||
|
||||
/** tertiary **/
|
||||
--ion-color-tertiary: #5260ff;
|
||||
--ion-color-tertiary-rgb: 82, 96, 255;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-tertiary-shade: #4854e0;
|
||||
--ion-color-tertiary-tint: #6370ff;
|
||||
|
||||
/** success **/
|
||||
--ion-color-success: #2dd36f;
|
||||
--ion-color-success-rgb: 45, 211, 111;
|
||||
--ion-color-success-contrast: #ffffff;
|
||||
--ion-color-success-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-success-shade: #28ba62;
|
||||
--ion-color-success-tint: #42d77d;
|
||||
|
||||
/** warning **/
|
||||
--ion-color-warning: #ffc409;
|
||||
--ion-color-warning-rgb: 255, 196, 9;
|
||||
--ion-color-warning-contrast: #000000;
|
||||
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-warning-shade: #e0ac08;
|
||||
--ion-color-warning-tint: #ffca22;
|
||||
|
||||
/** danger **/
|
||||
--ion-color-danger: #eb445a;
|
||||
--ion-color-danger-rgb: 235, 68, 90;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #cf3c4f;
|
||||
--ion-color-danger-tint: #ed576b;
|
||||
|
||||
/** dark **/
|
||||
--ion-color-dark: #222428;
|
||||
--ion-color-dark-rgb: 34, 36, 40;
|
||||
--ion-color-dark-contrast: #ffffff;
|
||||
--ion-color-dark-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-dark-shade: #1e2023;
|
||||
--ion-color-dark-tint: #383a3e;
|
||||
|
||||
/** medium **/
|
||||
--ion-color-medium: #92949c;
|
||||
--ion-color-medium-rgb: 146, 148, 156;
|
||||
--ion-color-medium-contrast: #ffffff;
|
||||
--ion-color-medium-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-medium-shade: #808289;
|
||||
--ion-color-medium-tint: #9d9fa6;
|
||||
|
||||
/** light **/
|
||||
--ion-color-light: #f4f5f8;
|
||||
--ion-color-light-rgb: 244, 245, 248;
|
||||
--ion-color-light-contrast: #000000;
|
||||
--ion-color-light-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-light-shade: #d7d8da;
|
||||
--ion-color-light-tint: #f5f6f9;
|
||||
}
|
||||
|
||||
.swiper-pagination {
|
||||
|
||||
margin-bottom: 1rem;
|
||||
}
|
@@ -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,37 @@
|
||||
import { IonContent, IonIcon, IonItem, IonLabel, IonList, IonListHeader, IonMenu, IonMenuToggle, IonNote } from '@ionic/react';
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { star, starOutline } from 'ionicons/icons';
|
||||
import './Menu.css';
|
||||
|
||||
const Menu = ({ pages }) => {
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<IonMenu contentId="main" type="overlay">
|
||||
<IonContent>
|
||||
<IonList id="inbox-list">
|
||||
<IonListHeader>Overlay Hooks</IonListHeader>
|
||||
<IonNote>Choose one below to see a demo</IonNote>
|
||||
|
||||
{ pages.map((appPage, index) => {
|
||||
|
||||
const isSelected = location.pathname === appPage.url;
|
||||
|
||||
return (
|
||||
<IonMenuToggle key={ index } autoHide={false}>
|
||||
<IonItem className={ isSelected ? 'selected' : '' } routerLink={ appPage.url } routerDirection="none" lines="none" detail={false}>
|
||||
<IonIcon slot="start" icon={ isSelected ? star : starOutline } />
|
||||
<IonLabel>{ appPage.label }</IonLabel>
|
||||
</IonItem>
|
||||
</IonMenuToggle>
|
||||
);
|
||||
})}
|
||||
</IonList>
|
||||
</IonContent>
|
||||
</IonMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default Menu;
|
@@ -0,0 +1,58 @@
|
||||
import { IonButton, IonButtons, IonContent, IonHeader, IonMenuButton, IonPage, IonTitle, IonToolbar, useIonActionSheet } from '@ionic/react';
|
||||
|
||||
const ActionSheet = () => {
|
||||
|
||||
const [ present, dismiss ] = useIonActionSheet();
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Action Sheet</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Action Sheet</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() =>
|
||||
present({
|
||||
buttons: [{ text: 'Ok' }, { text: 'Cancel' }],
|
||||
header: 'Action Sheet'
|
||||
})
|
||||
}
|
||||
>
|
||||
Show ActionSheet
|
||||
</IonButton>
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() =>
|
||||
present([{ text: 'Ok' }, { text: 'Cancel' }], 'Action Sheet')
|
||||
}
|
||||
>
|
||||
Show ActionSheet using params
|
||||
</IonButton>
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() => {
|
||||
present([{ text: 'Ok' }, { text: 'Cancel' }], 'Action Sheet');
|
||||
setTimeout(dismiss, 3000);
|
||||
}}
|
||||
>
|
||||
Show ActionSheet, hide after 3 seconds
|
||||
</IonButton>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionSheet;
|
@@ -0,0 +1,53 @@
|
||||
import { IonButton, IonButtons, IonContent, IonHeader, IonMenuButton, IonPage, IonTitle, IonToolbar, useIonAlert } from '@ionic/react';
|
||||
|
||||
const Alert = () => {
|
||||
|
||||
const [ present ] = useIonAlert();
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Alert</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Alert</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() =>
|
||||
present({
|
||||
cssClass: 'my-css',
|
||||
header: 'Alert',
|
||||
message: 'alert from hook',
|
||||
buttons: [
|
||||
'Cancel',
|
||||
{ text: 'Ok', handler: (d) => console.log('ok pressed') },
|
||||
],
|
||||
onDidDismiss: (e) => console.log('did dismiss'),
|
||||
})
|
||||
}
|
||||
>
|
||||
Show Alert
|
||||
</IonButton>
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() => present('hello with params', [{ text: 'Ok' }])}
|
||||
>
|
||||
Show Alert using params
|
||||
</IonButton>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Alert;
|
@@ -0,0 +1,54 @@
|
||||
import { IonButtons, IonCard, IonCardHeader, IonContent, IonHeader, IonMenuButton, IonPage, IonTitle, IonToolbar, IonCardTitle, IonCardSubtitle, IonCardContent, IonText } from '@ionic/react';
|
||||
|
||||
const All = () => {
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>All</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">All</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonCard>
|
||||
<IonCardHeader>
|
||||
<IonCardSubtitle>Sample usage</IonCardSubtitle>
|
||||
<IonCardTitle>Overlay Hooks</IonCardTitle>
|
||||
</IonCardHeader>
|
||||
|
||||
<IonCardContent>
|
||||
<IonText>
|
||||
<p>
|
||||
In Ionic React 5.6, the team packaged up a new set of hooks for controlling overlay components that they thought we might like. What is an overlay you ask? It’s the term that Ionic give components that display over your current content, such as alerts, modals, toasts, etc.
|
||||
</p>
|
||||
</IonText>
|
||||
<br />
|
||||
<IonText>
|
||||
<p>
|
||||
All of the code is taken from the Ionic Framework docs. You can find the blog post outlining these new overlay hooks <a href="https://ionicframework.com/blog/introducing-the-new-overlay-hooks-for-ionic-react/" target="_blank" rel="noreferrer">here.</a>
|
||||
</p>
|
||||
</IonText>
|
||||
<br />
|
||||
<IonText>
|
||||
<p>
|
||||
Check out the samples by navigating to a respective one in the side menu.
|
||||
</p>
|
||||
</IonText>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default All;
|
@@ -0,0 +1,46 @@
|
||||
import { IonButton, IonButtons, IonContent, IonHeader, IonMenuButton, IonPage, IonTitle, IonToolbar, useIonLoading } from '@ionic/react';
|
||||
|
||||
const Loading = () => {
|
||||
|
||||
const [ present ] = useIonLoading();
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Loading</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Loading</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() =>
|
||||
present({
|
||||
duration: 3000,
|
||||
})
|
||||
}
|
||||
>
|
||||
Show Loading
|
||||
</IonButton>
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() => present('Loading', 2000, 'dots')}
|
||||
>
|
||||
Show Loading using params
|
||||
</IonButton>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
@@ -0,0 +1,68 @@
|
||||
import { IonButton, IonButtons, IonContent, IonHeader, IonMenuButton, IonPage, IonText, IonTitle, IonToolbar, useIonModal } from '@ionic/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
const Modal = () => {
|
||||
|
||||
const Body = ({ count, onDismiss, onIncrement }) => (
|
||||
<div className="ion-text-center">
|
||||
<IonText color="dark" className="ion-text-center">Count: { count }</IonText>
|
||||
<IonButton expand="block" onClick={ () => onIncrement() }>
|
||||
Increment Count
|
||||
</IonButton>
|
||||
<IonButton expand="block" onClick={ () => onDismiss() }>
|
||||
Close
|
||||
</IonButton>
|
||||
</div>
|
||||
);
|
||||
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
const handleIncrement = () => {
|
||||
setCount(count + 1);
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
dismiss();
|
||||
};
|
||||
|
||||
const [present, dismiss] = useIonModal(Body, {
|
||||
count,
|
||||
onDismiss: handleDismiss,
|
||||
onIncrement: handleIncrement,
|
||||
});
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Modal</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Modal</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() => {
|
||||
present({
|
||||
cssClass: 'my-class',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Show Modal
|
||||
</IonButton>
|
||||
<div>Count: {count}</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
@@ -0,0 +1,97 @@
|
||||
import { IonButton, IonButtons, IonContent, IonHeader, IonMenuButton, IonPage, IonTitle, IonToolbar, useIonPicker } from '@ionic/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
const Picker = () => {
|
||||
|
||||
const [ present ] = useIonPicker();
|
||||
const [ value, setValue ] = useState('');
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Picker</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Picker</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() =>
|
||||
present({
|
||||
buttons: [
|
||||
{
|
||||
text: 'Confirm',
|
||||
handler: (selected) => {
|
||||
setValue(selected.animal.value)
|
||||
},
|
||||
},
|
||||
],
|
||||
columns: [
|
||||
{
|
||||
name: 'animal',
|
||||
options: [
|
||||
{ text: 'Dog', value: 'dog' },
|
||||
{ text: 'Cat', value: 'cat' },
|
||||
{ text: 'Bird', value: 'bird' },
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
>
|
||||
Show Picker
|
||||
</IonButton>
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() =>
|
||||
present(
|
||||
[
|
||||
{
|
||||
name: 'animal',
|
||||
options: [
|
||||
{ text: 'Dog', value: 'dog' },
|
||||
{ text: 'Cat', value: 'cat' },
|
||||
{ text: 'Bird', value: 'bird' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'vehicle',
|
||||
options: [
|
||||
{ text: 'Car', value: 'car' },
|
||||
{ text: 'Truck', value: 'truck' },
|
||||
{ text: 'Bike', value: 'bike' },
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
text: 'Confirm',
|
||||
handler: (selected) => {
|
||||
setValue(`${selected.animal.value}, ${selected.vehicle.value}`)
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
}
|
||||
>
|
||||
Show Picker using params
|
||||
</IonButton>
|
||||
{value && (
|
||||
<div>Selected Value: {value}</div>
|
||||
)}
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Picker;
|
@@ -0,0 +1,53 @@
|
||||
import { IonButtons, IonContent, IonHeader, IonItem, IonListHeader, IonMenuButton, IonPage, IonTitle, IonToolbar, IonList, useIonPopover, IonButton } from '@ionic/react';
|
||||
|
||||
const Popover = () => {
|
||||
|
||||
const PopoverList = ({ onHide }) => (
|
||||
<IonList>
|
||||
<IonListHeader>Ionic</IonListHeader>
|
||||
<IonItem button>Learn Ionic</IonItem>
|
||||
<IonItem button>Documentation</IonItem>
|
||||
<IonItem button>Showcase</IonItem>
|
||||
<IonItem button>GitHub Repo</IonItem>
|
||||
<IonItem lines="none" detail={false} button onClick={ onHide }>
|
||||
Close
|
||||
</IonItem>
|
||||
</IonList>
|
||||
);
|
||||
|
||||
const [ present, dismiss ] = useIonPopover(PopoverList, { onHide: () => dismiss() });
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Popover</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Popover</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={(e) =>
|
||||
present({
|
||||
event: e.nativeEvent,
|
||||
})
|
||||
}
|
||||
>
|
||||
Show Popover
|
||||
</IonButton>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Popover;
|
@@ -0,0 +1,52 @@
|
||||
import { IonButton, IonButtons, IonContent, IonHeader, IonMenuButton, IonPage, IonTitle, IonToolbar, useIonToast } from '@ionic/react';
|
||||
|
||||
const Toast = () => {
|
||||
|
||||
const [ present, dismiss ] = useIonToast();
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Toast</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Toast</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() =>
|
||||
present({
|
||||
buttons: [{ text: 'hide', handler: () => dismiss() }],
|
||||
message: 'toast from hook, click hide to dismiss',
|
||||
onDidDismiss: () => console.log('dismissed'),
|
||||
onWillDismiss: () => console.log('will dismiss'),
|
||||
})
|
||||
}
|
||||
>
|
||||
Show Toast
|
||||
</IonButton>
|
||||
<IonButton
|
||||
expand="block"
|
||||
onClick={() => present('hello from hook', 3000)}
|
||||
>
|
||||
Show Toast using params, closes in 3 secs
|
||||
</IonButton>
|
||||
<IonButton expand="block" onClick={dismiss}>
|
||||
Hide Toast
|
||||
</IonButton>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Toast;
|
@@ -0,0 +1,77 @@
|
||||
/* Ionic Variables and Theming. For more info, please see:
|
||||
http://ionicframework.com/docs/theming/ */
|
||||
|
||||
/** Ionic CSS Variables **/
|
||||
:root {
|
||||
/** primary **/
|
||||
--ion-color-primary: #3880ff;
|
||||
--ion-color-primary-rgb: 56, 128, 255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #3171e0;
|
||||
--ion-color-primary-tint: #4c8dff;
|
||||
|
||||
/** secondary **/
|
||||
--ion-color-secondary: #3dc2ff;
|
||||
--ion-color-secondary-rgb: 61, 194, 255;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #36abe0;
|
||||
--ion-color-secondary-tint: #50c8ff;
|
||||
|
||||
/** tertiary **/
|
||||
--ion-color-tertiary: #5260ff;
|
||||
--ion-color-tertiary-rgb: 82, 96, 255;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-tertiary-shade: #4854e0;
|
||||
--ion-color-tertiary-tint: #6370ff;
|
||||
|
||||
/** success **/
|
||||
--ion-color-success: #2dd36f;
|
||||
--ion-color-success-rgb: 45, 211, 111;
|
||||
--ion-color-success-contrast: #ffffff;
|
||||
--ion-color-success-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-success-shade: #28ba62;
|
||||
--ion-color-success-tint: #42d77d;
|
||||
|
||||
/** warning **/
|
||||
--ion-color-warning: #ffc409;
|
||||
--ion-color-warning-rgb: 255, 196, 9;
|
||||
--ion-color-warning-contrast: #000000;
|
||||
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-warning-shade: #e0ac08;
|
||||
--ion-color-warning-tint: #ffca22;
|
||||
|
||||
/** danger **/
|
||||
--ion-color-danger: #eb445a;
|
||||
--ion-color-danger-rgb: 235, 68, 90;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #cf3c4f;
|
||||
--ion-color-danger-tint: #ed576b;
|
||||
|
||||
/** dark **/
|
||||
--ion-color-dark: #222428;
|
||||
--ion-color-dark-rgb: 34, 36, 40;
|
||||
--ion-color-dark-contrast: #ffffff;
|
||||
--ion-color-dark-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-dark-shade: #1e2023;
|
||||
--ion-color-dark-tint: #383a3e;
|
||||
|
||||
/** medium **/
|
||||
--ion-color-medium: #92949c;
|
||||
--ion-color-medium-rgb: 146, 148, 156;
|
||||
--ion-color-medium-contrast: #ffffff;
|
||||
--ion-color-medium-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-medium-shade: #808289;
|
||||
--ion-color-medium-tint: #9d9fa6;
|
||||
|
||||
/** light **/
|
||||
--ion-color-light: #f4f5f8;
|
||||
--ion-color-light-rgb: 244, 245, 248;
|
||||
--ion-color-light-contrast: #000000;
|
||||
--ion-color-light-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-light-shade: #d7d8da;
|
||||
--ion-color-light-tint: #f5f6f9;
|
||||
}
|
113
03_source/mobile/src/pages/DemoReactQuotes/components/Menu.css
Normal file
113
03_source/mobile/src/pages/DemoReactQuotes/components/Menu.css
Normal file
@@ -0,0 +1,113 @@
|
||||
ion-menu ion-content {
|
||||
--background: var(--ion-item-background, var(--ion-background-color, #fff));
|
||||
}
|
||||
|
||||
ion-menu.md ion-content {
|
||||
--padding-start: 8px;
|
||||
--padding-end: 8px;
|
||||
--padding-top: 20px;
|
||||
--padding-bottom: 20px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-list {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
ion-menu.md ion-note {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-list-header, ion-menu.md ion-note {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-list#inbox-list {
|
||||
border-bottom: 1px solid var(--ion-color-step-150, #d7d8da);
|
||||
}
|
||||
|
||||
ion-menu.md ion-list#inbox-list ion-list-header {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-list#labels-list ion-list-header {
|
||||
font-size: 16px;
|
||||
margin-bottom: 18px;
|
||||
color: #757575;
|
||||
min-height: 26px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-item {
|
||||
--padding-start: 10px;
|
||||
--padding-end: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
ion-menu.md ion-item.selected {
|
||||
--background: rgba(var(--ion-color-primary-rgb), 0.14);
|
||||
}
|
||||
|
||||
ion-menu.md ion-item.selected ion-icon {
|
||||
color: var(--ion-color-primary);
|
||||
}
|
||||
|
||||
ion-menu.md ion-item ion-icon {
|
||||
color: #616e7e;
|
||||
}
|
||||
|
||||
ion-menu.md ion-item ion-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-content {
|
||||
--padding-bottom: 20px;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-list {
|
||||
padding: 20px 0 0 0;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-note {
|
||||
line-height: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-item {
|
||||
--padding-start: 16px;
|
||||
--padding-end: 16px;
|
||||
--min-height: 50px;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-item ion-icon {
|
||||
font-size: 24px;
|
||||
color: #73849a;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-item .selected ion-icon {
|
||||
color: var(--ion-color-primary);
|
||||
}
|
||||
|
||||
ion-menu.ios ion-list#labels-list ion-list-header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-list-header,
|
||||
ion-menu.ios ion-note {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
ion-menu.ios ion-note {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
ion-note {
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
color: var(--ion-color-medium-shade);
|
||||
}
|
||||
|
||||
ion-item.selected {
|
||||
--color: var(--ion-color-primary);
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
IonContent,
|
||||
IonIcon,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonList,
|
||||
IonListHeader,
|
||||
IonMenu,
|
||||
IonMenuToggle,
|
||||
IonNote,
|
||||
} from '@ionic/react';
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { bookmarkOutline, bookmarkSharp, homeOutline, homeSharp } from 'ionicons/icons';
|
||||
import './Menu.css';
|
||||
import { useStoreState } from 'pullstate';
|
||||
import { QuoteStore } from '../store';
|
||||
import { getSavedQuotes } from '../store/Selectors';
|
||||
|
||||
const Menu = () => {
|
||||
|
||||
const location = useLocation();
|
||||
const saved = useStoreState(QuoteStore, getSavedQuotes);
|
||||
|
||||
const appPages = [
|
||||
{
|
||||
title: 'Home',
|
||||
url: '/home',
|
||||
iosIcon: homeOutline,
|
||||
mdIcon: homeSharp
|
||||
},
|
||||
{
|
||||
title: `Bookmarks (${ saved.length })`,
|
||||
url: '/saved',
|
||||
iosIcon: bookmarkOutline,
|
||||
mdIcon: bookmarkSharp
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<IonMenu contentId="main" type="overlay">
|
||||
<IonContent>
|
||||
<IonList id="inbox-list" className="ion-margin-top">
|
||||
<IonListHeader>Ionic Quotes</IonListHeader>
|
||||
<IonNote>hey there!</IonNote>
|
||||
{appPages.map((appPage, index) => {
|
||||
return (
|
||||
<IonMenuToggle key={index} autoHide={false}>
|
||||
<IonItem className={location.pathname === appPage.url ? 'selected' : ''} routerLink={appPage.url} routerDirection="none" lines="none" detail={false}>
|
||||
<IonIcon slot="start" ios={appPage.iosIcon} md={appPage.mdIcon} />
|
||||
<IonLabel>{appPage.title}</IonLabel>
|
||||
</IonItem>
|
||||
</IonMenuToggle>
|
||||
);
|
||||
})}
|
||||
</IonList>
|
||||
</IonContent>
|
||||
</IonMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default Menu;
|
@@ -0,0 +1,21 @@
|
||||
.quoteItem {
|
||||
|
||||
--quote-item-background: rgb(49, 117, 226);
|
||||
|
||||
border: 2px solid rgb(154, 204, 245);
|
||||
border-radius: 10px;
|
||||
--background: var(--quote-item-background);
|
||||
background: var(--quote-item-background);
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.quoteText p {
|
||||
|
||||
color: rgb(25, 51, 93);
|
||||
}
|
||||
|
||||
.quoteText h1:hover {
|
||||
|
||||
color: white;
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
import { IonCol, IonItem, IonLabel } from "@ionic/react";
|
||||
import styles from "./QuoteItem.module.css";
|
||||
|
||||
export const QuoteItem = ({ quote }) => {
|
||||
|
||||
return (
|
||||
|
||||
<IonCol size="6" className="animate__animated animate__fadeIn">
|
||||
<IonItem lines="none" className={ styles.quoteItem } routerLink={ `/quote/${ quote.id }`}>
|
||||
<IonLabel className={ styles.quoteText }>
|
||||
<h2>{ quote.text }</h2>
|
||||
<p>{ quote.author }</p>
|
||||
</IonLabel>
|
||||
</IonItem>
|
||||
</IonCol>
|
||||
);
|
||||
}
|
62
03_source/mobile/src/pages/DemoReactQuotes/pages/Home.tsx
Normal file
62
03_source/mobile/src/pages/DemoReactQuotes/pages/Home.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { IonButtons, IonContent, IonGrid, IonHeader, IonInfiniteScroll, IonInfiniteScrollContent, IonList, IonMenuButton, IonPage, IonRow, IonTitle, IonToolbar } from '@ionic/react';
|
||||
import { useStoreState } from 'pullstate';
|
||||
import { useState } from 'react';
|
||||
import { QuoteItem } from '../components/QuoteItem';
|
||||
import { QuoteStore } from '../store';
|
||||
import { getQuotes } from '../store/Selectors';
|
||||
|
||||
const Home = () => {
|
||||
|
||||
const quotes = useStoreState(QuoteStore, getQuotes);
|
||||
const [ amountLoaded, setAmountLoaded ] = useState(20);
|
||||
|
||||
const fetchMore = async e => {
|
||||
|
||||
setAmountLoaded(amountLoaded => amountLoaded + 20);
|
||||
e.target.complete();
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Home</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Home</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonGrid>
|
||||
<IonList>
|
||||
<IonRow>
|
||||
{ quotes.map((quote, index) => {
|
||||
|
||||
if ((index <= amountLoaded) && quote.author) {
|
||||
return (
|
||||
|
||||
<QuoteItem key={ index } quote={ quote } />
|
||||
);
|
||||
} else return "";
|
||||
})}
|
||||
</IonRow>
|
||||
</IonList>
|
||||
|
||||
<IonInfiniteScroll threshold="200px" onIonInfinite={ fetchMore }>
|
||||
<IonInfiniteScrollContent loadingSpinner="bubbles" loadingText="Getting more quotes...">
|
||||
</IonInfiniteScrollContent>
|
||||
</IonInfiniteScroll>
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
88
03_source/mobile/src/pages/DemoReactQuotes/pages/Quote.tsx
Normal file
88
03_source/mobile/src/pages/DemoReactQuotes/pages/Quote.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { IonBackButton, IonButton, IonButtons, IonCard, IonCardContent, IonCol, IonContent, IonHeader, IonIcon, IonImg, IonPage, IonRow, IonTitle, IonToolbar, useIonToast } from '@ionic/react';
|
||||
import { bookmarkOutline, checkmarkOutline, copyOutline } from 'ionicons/icons';
|
||||
import { useStoreState } from 'pullstate';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { QuoteStore } from '../store';
|
||||
import { addSavedQuote, removeSavedQuote } from '../store/QuoteStore';
|
||||
import { getQuote, getSavedQuotes } from '../store/Selectors';
|
||||
|
||||
import { Clipboard } from '@capacitor/clipboard';
|
||||
|
||||
const Quote = () => {
|
||||
|
||||
const { id } = useParams();
|
||||
const quote = useStoreState(QuoteStore, getQuote(id));
|
||||
const saved = useStoreState(QuoteStore, getSavedQuotes);
|
||||
const [ bookmarked, setBookmarked ] = useState(false);
|
||||
|
||||
const [ present ] = useIonToast();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
setBookmarked(saved.includes(parseInt(id)));
|
||||
}, [ saved, id ]);
|
||||
|
||||
const copyQuote = async () => {
|
||||
|
||||
await Clipboard.write({
|
||||
|
||||
string: quote.text
|
||||
});
|
||||
|
||||
present({
|
||||
|
||||
header: "Success",
|
||||
message: "Quote copied to clipboard!",
|
||||
duration: 2500,
|
||||
color: "primary"
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonBackButton text="Home" />
|
||||
</IonButtons>
|
||||
<IonTitle>Quote</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Quote</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonCard className="animate__animated animate__slideInRight animate__faster">
|
||||
<IonImg src={ quote.image } alt="quote cover" />
|
||||
<IonCardContent>
|
||||
<h1>{ quote.text }</h1>
|
||||
<p>- { quote.author }</p>
|
||||
</IonCardContent>
|
||||
|
||||
<IonRow>
|
||||
<IonCol size="6">
|
||||
<IonButton fill={ !bookmarked ? "outline" : "solid" } onClick={ () => bookmarked ? removeSavedQuote(quote.id) : addSavedQuote(quote.id) }>
|
||||
<IonIcon icon={ bookmarked ? checkmarkOutline : bookmarkOutline } />
|
||||
{ bookmarked ? "Bookmarked" : "Save as Bookmark" }
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="4">
|
||||
<IonButton fill="outline" onClick={ copyQuote }>
|
||||
<IonIcon icon={ copyOutline } />
|
||||
Copy Quote
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonCard>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Quote;
|
74
03_source/mobile/src/pages/DemoReactQuotes/pages/Saved.tsx
Normal file
74
03_source/mobile/src/pages/DemoReactQuotes/pages/Saved.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { IonButtons, IonCol, IonContent, IonGrid, IonHeader, IonInfiniteScroll, IonInfiniteScrollContent, IonList, IonMenuButton, IonPage, IonRow, IonTitle, IonToolbar } from '@ionic/react';
|
||||
import { useStoreState } from 'pullstate';
|
||||
import { useState } from 'react';
|
||||
import { QuoteItem } from '../components/QuoteItem';
|
||||
import { QuoteStore } from '../store';
|
||||
import { getQuotes, getSavedQuotes } from '../store/Selectors';
|
||||
|
||||
const Saved = () => {
|
||||
|
||||
const quotes = useStoreState(QuoteStore, getQuotes);
|
||||
const saved = useStoreState(QuoteStore, getSavedQuotes);
|
||||
const [ amountLoaded, setAmountLoaded ] = useState(20);
|
||||
|
||||
const fetchMore = async e => {
|
||||
|
||||
setAmountLoaded(amountLoaded => amountLoaded + 20);
|
||||
e.target.complete();
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Bookmarks</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Bookmarks</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonGrid>
|
||||
{ quotes.length > 0 &&
|
||||
|
||||
<IonList>
|
||||
<IonRow>
|
||||
{ quotes.map((quote, index) => {
|
||||
|
||||
if ((index <= amountLoaded) && saved.includes(parseInt(quote.id))) {
|
||||
return (
|
||||
|
||||
<QuoteItem key={ index } quote={ quote } />
|
||||
);
|
||||
} else return "";
|
||||
})}
|
||||
|
||||
<IonInfiniteScroll threshold="200px" onIonInfinite={ fetchMore }>
|
||||
<IonInfiniteScrollContent loadingSpinner="bubbles" loadingText="Getting more quotes...">
|
||||
</IonInfiniteScrollContent>
|
||||
</IonInfiniteScroll>
|
||||
</IonRow>
|
||||
</IonList>
|
||||
}
|
||||
|
||||
{ quotes.length < 1 &&
|
||||
<IonRow>
|
||||
<IonCol size="12">
|
||||
<h3>You haven't saved any bookmarks yet.</h3>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
}
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Saved;
|
@@ -0,0 +1,33 @@
|
||||
import { Store } from "pullstate";
|
||||
|
||||
const QuoteStore = new Store({
|
||||
|
||||
quotes: [],
|
||||
saved: []
|
||||
});
|
||||
|
||||
export default QuoteStore;
|
||||
|
||||
export const addSavedQuote = id => {
|
||||
|
||||
QuoteStore.update(s => { s.saved = [ ...s.saved, id ] });
|
||||
}
|
||||
|
||||
export const removeSavedQuote = id => {
|
||||
|
||||
QuoteStore.update(s => { s.saved = s.saved.filter(savedId => parseInt(savedId) !== parseInt(id)) });
|
||||
}
|
||||
|
||||
export const fetchQuotes = async () => {
|
||||
|
||||
const response = await fetch("https://type.fit/api/quotes");
|
||||
const data = await response.json();
|
||||
|
||||
await data.filter((quote, index) => {
|
||||
|
||||
quote.id = (Date.now() + index);
|
||||
quote.image = `https://source.unsplash.com/random/1200x400?sig=${ quote.id }`;
|
||||
});
|
||||
|
||||
QuoteStore.update(s => { s.quotes = data });
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
const getState = state => state;
|
||||
|
||||
// General getters
|
||||
export const getQuotes = createSelector(getState, state => state.quotes);
|
||||
export const getSavedQuotes = createSelector(getState, state => state.saved);
|
||||
|
||||
// Specific getters
|
||||
export const getQuote = id => createSelector(getState, state => state.quotes.filter(q => parseInt(q.id) === parseInt(id))[0]);
|
@@ -0,0 +1 @@
|
||||
export { default as QuoteStore } from "./QuoteStore";
|
@@ -0,0 +1,77 @@
|
||||
/* Ionic Variables and Theming. For more info, please see:
|
||||
http://ionicframework.com/docs/theming/ */
|
||||
|
||||
/** Ionic CSS Variables **/
|
||||
:root {
|
||||
/** primary **/
|
||||
--ion-color-primary: #3880ff;
|
||||
--ion-color-primary-rgb: 56, 128, 255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #3171e0;
|
||||
--ion-color-primary-tint: #4c8dff;
|
||||
|
||||
/** secondary **/
|
||||
--ion-color-secondary: #3dc2ff;
|
||||
--ion-color-secondary-rgb: 61, 194, 255;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #36abe0;
|
||||
--ion-color-secondary-tint: #50c8ff;
|
||||
|
||||
/** tertiary **/
|
||||
--ion-color-tertiary: #5260ff;
|
||||
--ion-color-tertiary-rgb: 82, 96, 255;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-tertiary-shade: #4854e0;
|
||||
--ion-color-tertiary-tint: #6370ff;
|
||||
|
||||
/** success **/
|
||||
--ion-color-success: #2dd36f;
|
||||
--ion-color-success-rgb: 45, 211, 111;
|
||||
--ion-color-success-contrast: #ffffff;
|
||||
--ion-color-success-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-success-shade: #28ba62;
|
||||
--ion-color-success-tint: #42d77d;
|
||||
|
||||
/** warning **/
|
||||
--ion-color-warning: #ffc409;
|
||||
--ion-color-warning-rgb: 255, 196, 9;
|
||||
--ion-color-warning-contrast: #000000;
|
||||
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-warning-shade: #e0ac08;
|
||||
--ion-color-warning-tint: #ffca22;
|
||||
|
||||
/** danger **/
|
||||
--ion-color-danger: #eb445a;
|
||||
--ion-color-danger-rgb: 235, 68, 90;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #cf3c4f;
|
||||
--ion-color-danger-tint: #ed576b;
|
||||
|
||||
/** dark **/
|
||||
--ion-color-dark: #222428;
|
||||
--ion-color-dark-rgb: 34, 36, 40;
|
||||
--ion-color-dark-contrast: #ffffff;
|
||||
--ion-color-dark-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-dark-shade: #1e2023;
|
||||
--ion-color-dark-tint: #383a3e;
|
||||
|
||||
/** medium **/
|
||||
--ion-color-medium: #92949c;
|
||||
--ion-color-medium-rgb: 146, 148, 156;
|
||||
--ion-color-medium-contrast: #ffffff;
|
||||
--ion-color-medium-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-medium-shade: #808289;
|
||||
--ion-color-medium-tint: #9d9fa6;
|
||||
|
||||
/** light **/
|
||||
--ion-color-light: #f4f5f8;
|
||||
--ion-color-light-rgb: 244, 245, 248;
|
||||
--ion-color-light-contrast: #000000;
|
||||
--ion-color-light-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-light-shade: #d7d8da;
|
||||
--ion-color-light-tint: #f5f6f9;
|
||||
}
|
@@ -15,7 +15,7 @@ import MembersNearByList from '../MembersNearByList';
|
||||
import OrderList from '../OrderList';
|
||||
import MyProfile from '../MyProfile';
|
||||
import MessageList from '../MessageList';
|
||||
import paths from '../../paths';
|
||||
import PATHS from '../../PATHS';
|
||||
import Favourites from '../Favourites';
|
||||
import TabAppRoute from '../../TabAppRoute';
|
||||
|
||||
@@ -53,23 +53,23 @@ const MainTabs: React.FC<MainTabsProps> = () => {
|
||||
</IonTabButton>
|
||||
*/}
|
||||
|
||||
<IonTabButton tab="events" href={paths.EVENT_LIST}>
|
||||
<IonTabButton tab="events" href={PATHS.EVENT_LIST}>
|
||||
<IonIcon icon={calendar} />
|
||||
<IonLabel>Event</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="nearby" href={paths.NEARBY_LIST}>
|
||||
<IonTabButton tab="nearby" href={PATHS.NEARBY_LIST}>
|
||||
<IonIcon icon={people} />
|
||||
<IonLabel>Nearby</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="orders" href={paths.ORDERS_LIST}>
|
||||
<IonTabButton tab="orders" href={PATHS.ORDERS_LIST}>
|
||||
<IonIcon icon={location} />
|
||||
<IonLabel>Order</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="message" href={paths.MESSAGE_LIST}>
|
||||
<IonTabButton tab="message" href={PATHS.MESSAGE_LIST}>
|
||||
<IonIcon icon={informationCircle} />
|
||||
<IonLabel>Message</IonLabel>
|
||||
</IonTabButton>
|
||||
<IonTabButton tab="my_profile" href={paths.PROFILE}>
|
||||
<IonTabButton tab="my_profile" href={PATHS.PROFILE}>
|
||||
<IonIcon icon={informationCircle} />
|
||||
<IonLabel>Profile</IonLabel>
|
||||
</IonTabButton>
|
||||
|
@@ -30,7 +30,7 @@ import { capitalize, productInfo } from '../utils';
|
||||
const ProductType = () => {
|
||||
const router = useIonRouter();
|
||||
const { category, type } = useParams();
|
||||
const productsRef = useRef();
|
||||
const productsRef = useRef(null);
|
||||
|
||||
const [products, setProducts] = useState([]);
|
||||
const [filteredProducts, setFilteredProducts] = useState([]);
|
||||
|
@@ -1,58 +1,50 @@
|
||||
import { CreateAnimation, IonButton, IonIcon } from "@ionic/react";
|
||||
import { cartOutline } from "ionicons/icons";
|
||||
import { useRef, useState } from "react";
|
||||
import { addToCart } from "../store/CartStore";
|
||||
import { CreateAnimation, IonButton, IonIcon } from '@ionic/react';
|
||||
import { cartOutline } from 'ionicons/icons';
|
||||
import { useRef, useState } from 'react';
|
||||
import { addToCart } from '../store/CartStore';
|
||||
|
||||
export const AddToCartButton = ({product}) => {
|
||||
export const AddToCartButton = ({ product }) => {
|
||||
const animationRef = useRef(null);
|
||||
const [hidden, setHidden] = useState(true);
|
||||
|
||||
const animationRef = useRef();
|
||||
const [hidden, setHidden] = useState(true);
|
||||
const floatStyle = {
|
||||
display: hidden ? 'none' : '',
|
||||
position: 'absolute',
|
||||
};
|
||||
|
||||
const floatStyle = {
|
||||
const floatGrowAnimation = {
|
||||
property: 'transform',
|
||||
fromValue: 'translateY(0) scale(1)',
|
||||
toValue: 'translateY(-55px) scale(1.5)',
|
||||
};
|
||||
|
||||
display: hidden ? "none" : "",
|
||||
position: "absolute"
|
||||
};
|
||||
const colorAnimation = {
|
||||
property: 'color',
|
||||
fromValue: 'green',
|
||||
toValue: 'green',
|
||||
};
|
||||
|
||||
const floatGrowAnimation = {
|
||||
const mainAnimation = {
|
||||
duration: 1500,
|
||||
iterations: '1',
|
||||
fromTo: [floatGrowAnimation, colorAnimation],
|
||||
easing: 'cubic-bezier(0.25, 0.7, 0.25, 0.7)',
|
||||
};
|
||||
|
||||
property: "transform",
|
||||
fromValue: "translateY(0) scale(1)",
|
||||
toValue: "translateY(-55px) scale(1.5)"
|
||||
};
|
||||
const handleAddToCart = async (product) => {
|
||||
setHidden(false);
|
||||
await animationRef.current.animation.play();
|
||||
setHidden(true);
|
||||
addToCart(product);
|
||||
};
|
||||
|
||||
const colorAnimation = {
|
||||
|
||||
property: "color",
|
||||
fromValue: "green",
|
||||
toValue: "green"
|
||||
};
|
||||
|
||||
const mainAnimation = {
|
||||
|
||||
duration: 1500,
|
||||
iterations: "1",
|
||||
fromTo: [ floatGrowAnimation, colorAnimation ],
|
||||
easing: "cubic-bezier(0.25, 0.7, 0.25, 0.7)"
|
||||
};
|
||||
|
||||
const handleAddToCart = async product => {
|
||||
|
||||
setHidden(false);
|
||||
await animationRef.current.animation.play();
|
||||
setHidden(true);
|
||||
addToCart(product);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<IonButton color="dark" expand="full" onClick={() => handleAddToCart(product)}>
|
||||
<IonIcon icon={cartOutline} />
|
||||
Add to Cart
|
||||
|
||||
<CreateAnimation ref={animationRef} {...mainAnimation}>
|
||||
<IonIcon icon={cartOutline} size="large" style={floatStyle} />
|
||||
</CreateAnimation>
|
||||
</IonButton>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<IonButton color="dark" expand="full" onClick={() => handleAddToCart(product)}>
|
||||
<IonIcon icon={cartOutline} />
|
||||
Add to Cart
|
||||
<CreateAnimation ref={animationRef} {...mainAnimation}>
|
||||
<IonIcon icon={cartOutline} size="large" style={floatStyle} />
|
||||
</CreateAnimation>
|
||||
</IonButton>
|
||||
);
|
||||
};
|
||||
|
@@ -18,7 +18,7 @@ import SpeakerDetail from '../SpeakerDetail';
|
||||
import SessionDetail from '../SessionDetail';
|
||||
import MapView from '../MapView';
|
||||
import About from '../About';
|
||||
import paths from '../../paths';
|
||||
import PATHS from '../../PATHS';
|
||||
import TabAppRoute from '../../TabAppRoute';
|
||||
import { CartStore } from './store';
|
||||
import { getCartCount } from './store/Selectors';
|
||||
|
@@ -0,0 +1,58 @@
|
||||
import { CreateAnimation, IonButton, IonIcon } from "@ionic/react";
|
||||
import { cartOutline } from "ionicons/icons";
|
||||
import { useRef, useState } from "react";
|
||||
import { addToCart } from "../store/CartStore";
|
||||
|
||||
export const AddToCartButton = ({product}) => {
|
||||
|
||||
const animationRef = useRef();
|
||||
const [hidden, setHidden] = useState(true);
|
||||
|
||||
const floatStyle = {
|
||||
|
||||
display: hidden ? "none" : "",
|
||||
position: "absolute"
|
||||
};
|
||||
|
||||
const floatGrowAnimation = {
|
||||
|
||||
property: "transform",
|
||||
fromValue: "translateY(0) scale(1)",
|
||||
toValue: "translateY(-55px) scale(1.5)"
|
||||
};
|
||||
|
||||
const colorAnimation = {
|
||||
|
||||
property: "color",
|
||||
fromValue: "green",
|
||||
toValue: "green"
|
||||
};
|
||||
|
||||
const mainAnimation = {
|
||||
|
||||
duration: 1500,
|
||||
iterations: "1",
|
||||
fromTo: [ floatGrowAnimation, colorAnimation ],
|
||||
easing: "cubic-bezier(0.25, 0.7, 0.25, 0.7)"
|
||||
};
|
||||
|
||||
const handleAddToCart = async product => {
|
||||
|
||||
setHidden(false);
|
||||
await animationRef.current.animation.play();
|
||||
setHidden(true);
|
||||
addToCart(product);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<IonButton color="dark" expand="full" onClick={() => handleAddToCart(product)}>
|
||||
<IonIcon icon={cartOutline} />
|
||||
Add to Cart
|
||||
|
||||
<CreateAnimation ref={animationRef} {...mainAnimation}>
|
||||
<IonIcon icon={cartOutline} size="large" style={floatStyle} />
|
||||
</CreateAnimation>
|
||||
</IonButton>
|
||||
);
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
import { IonBreadcrumb, IonBreadcrumbs, IonIcon } from "@ionic/react";
|
||||
import { fastFoodOutline } from "ionicons/icons";
|
||||
import { useState } from "react";
|
||||
|
||||
export const Breadcrumbs = () => {
|
||||
|
||||
const [maxItems, setMaxItems] = useState(2);
|
||||
|
||||
const handleClick = () => {
|
||||
|
||||
setMaxItems(undefined);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<IonBreadcrumbs maxItems={maxItems} onIonCollapsedClick={handleClick}>
|
||||
<IonBreadcrumb color="medium">Page 1</IonBreadcrumb>
|
||||
<IonBreadcrumb color="medium">Page 2</IonBreadcrumb>
|
||||
<IonBreadcrumb color="medium">Page 3</IonBreadcrumb>
|
||||
<IonBreadcrumb>Page 4</IonBreadcrumb>
|
||||
</IonBreadcrumbs>
|
||||
);
|
||||
}
|
@@ -0,0 +1,82 @@
|
||||
import { useStoreState } from "pullstate";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CartStore } from "../store";
|
||||
import { addToCart } from "../store/CartStore";
|
||||
import { getCart } from "../store/Selectors";
|
||||
|
||||
const { IonPage, IonHeader, IonToolbar, IonTitle, IonButtons, IonIcon, IonContent, IonGrid, IonRow, IonItem, IonLabel, IonText, IonThumbnail, IonFooter, IonCol, IonButton, IonItemSliding, IonItemOptions, IonItemOption } = require("@ionic/react");
|
||||
const { close } = require("ionicons/icons");
|
||||
|
||||
export const CartModal = props => {
|
||||
|
||||
const cart = useStoreState(CartStore, getCart);
|
||||
const [totalPrice, setTotalPrice] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
let total = 0;
|
||||
cart.forEach(item => total += parseInt(item.price.replace("£", "")));
|
||||
setTotalPrice(total);
|
||||
}, [cart]);
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Cart</IonTitle>
|
||||
<IonButtons slot="end" onClick={props.close}>
|
||||
<IonIcon icon={close} size="large" />
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent>
|
||||
|
||||
<IonGrid>
|
||||
<IonRow style={{borderBottom: "1px solid black"}} className="ion-margin-bottom">
|
||||
<IonItem lines="none">
|
||||
<IonLabel>
|
||||
<h1>{cart.length} products in your cart</h1>
|
||||
<IonText color="medium">
|
||||
<h2>Review products and checkout</h2>
|
||||
</IonText>
|
||||
</IonLabel>
|
||||
</IonItem>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
|
||||
{cart.map((item, index) => (
|
||||
<IonItemSliding>
|
||||
<IonItem key={index} lines="none" className="ion-padding-end" style={{paddingTop: "0.75rem", paddingBottom: "0.75rem"}}>
|
||||
<IonThumbnail>
|
||||
<img src={item.image} alt="item" />
|
||||
</IonThumbnail>
|
||||
<IonLabel className="ion-padding-start ion-text-wrap">
|
||||
<h2>{item.title}</h2>
|
||||
<p>{item.price}</p>
|
||||
</IonLabel>
|
||||
</IonItem>
|
||||
|
||||
<IonItemOptions side="end">
|
||||
<IonItemOption color="danger" onClick={() => addToCart(item)}>
|
||||
Remove
|
||||
</IonItemOption>
|
||||
</IonItemOptions>
|
||||
</IonItemSliding>
|
||||
))}
|
||||
</IonContent>
|
||||
|
||||
<IonFooter className="ion-padding-bottom ion-padding-start ion-padding-end" style={{paddingBottom: "3rem"}}>
|
||||
<IonRow className="ion-justify-content-between">
|
||||
<IonCol size="8">
|
||||
<h1>Total</h1>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="4">
|
||||
<h1>£{totalPrice.toFixed(2)}</h1>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
<IonButton expand="block" color="dark">Checkout →</IonButton>
|
||||
</IonFooter>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
import { IonButton, IonCol, IonContent, IonGrid, IonHeader, IonRow, IonTitle, IonToolbar } from "@ionic/react";
|
||||
|
||||
export const FilterModal = ({productsRef, filterCriteria, setFilterCriteria, dismiss, filters}) => {
|
||||
|
||||
const filterProducts = async filter => {
|
||||
|
||||
await productsRef.current.classList.add("animate__fadeOutLeft");
|
||||
|
||||
setTimeout(() => {
|
||||
productsRef.current.classList.remove("animate__fadeOutLeft");
|
||||
productsRef.current.classList.add("animate__fadeInRight");
|
||||
setFilterCriteria(filter);
|
||||
}, 500);
|
||||
dismiss();
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<IonContent>
|
||||
<IonHeader>
|
||||
<IonToolbar color="none" style={{"--border-style": "none"}}>
|
||||
<IonTitle className="ion-margin-top">Filter</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonGrid>
|
||||
<IonRow>
|
||||
{filters.map(f => (
|
||||
<IonCol key={f} size="3">
|
||||
<IonButton expand="full" color={filterCriteria === f ? "dark" : "light"} onClick={() => filterProducts(f)}>{f}</IonButton>
|
||||
</IonCol>
|
||||
))}
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
);
|
||||
}
|
@@ -0,0 +1,88 @@
|
||||
ion-card {
|
||||
margin: 0;
|
||||
/* margin-top: var(--ion-safe-area-top); */
|
||||
z-index: -1;
|
||||
|
||||
border-radius: 0px;
|
||||
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
box-shadow: none;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
@supports not (aspect-ratio: 1 / 1) {
|
||||
ion-card::before {
|
||||
float: left;
|
||||
padding-top: 100%;
|
||||
content: '';
|
||||
}
|
||||
|
||||
ion-card::after {
|
||||
display: block;
|
||||
content: '';
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
ion-card-header {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
/* background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.7) 100%); */
|
||||
background: rgba(0, 0, 0, 0.5)
|
||||
}
|
||||
|
||||
ion-card-title,
|
||||
ion-card-subtitle {
|
||||
color: white;
|
||||
}
|
||||
|
||||
ion-card-header ion-card-title {
|
||||
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
ion-card-header ion-card-subtitle {
|
||||
|
||||
text-transform: none;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
ion-card-content {
|
||||
height: calc(60px + var(--ion-safe-area-top));
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.5) 100%);
|
||||
}
|
||||
|
||||
#close-button {
|
||||
position: fixed;
|
||||
|
||||
top: max(var(--ion-safe-area-top), 16px);
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
#fave-button {
|
||||
position: fixed;
|
||||
|
||||
top: max(var(--ion-safe-area-top), 16px);
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
#product-view-buttons {
|
||||
|
||||
z-index: 10;
|
||||
background: linear-gradient(360deg, rgba(0, 0, 0, 0) 0%, rgba(82, 82, 82, 0.9) 100%) !important;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
.sticky-bottom {
|
||||
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
}
|
@@ -0,0 +1,76 @@
|
||||
import { IonButton, IonButtons, IonCard, IonCardHeader, IonCardSubtitle, IonCardTitle, IonCol, IonContent, IonFooter, IonIcon, IonLabel, IonNote, IonRow, IonText, IonToolbar } from "@ionic/react";
|
||||
import { closeCircle, heart, heartOutline } from "ionicons/icons";
|
||||
import { useStoreState } from "pullstate";
|
||||
import { useRef } from "react";
|
||||
|
||||
import { checkFavourites } from "../store/Selectors";
|
||||
import { addToFavourites } from "../store/FavouritesStore";
|
||||
import { FavouritesStore } from "../store";
|
||||
|
||||
import "./ProductModal.css";
|
||||
import { ProductReviews } from "./ProductReviews";
|
||||
import { ProductSpecificationsAccordion } from "./ProductSpecificationsAccordion";
|
||||
import { AddToCartButton } from "./AddToCartButton";
|
||||
|
||||
export const ProductModal = props => {
|
||||
|
||||
const { dismiss, category = false, product } = props;
|
||||
const isFavourite = useStoreState(FavouritesStore, checkFavourites(product));
|
||||
const contentRef = useRef(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IonContent ref={contentRef}>
|
||||
<IonButtons id="product-view-buttons">
|
||||
<IonButton color="light" onClick={dismiss} id="close-button">
|
||||
<IonIcon icon={closeCircle} size="large" />
|
||||
</IonButton>
|
||||
|
||||
<IonButton color="danger" onClick={() => addToFavourites(product, category)} id="fave-button">
|
||||
<IonIcon icon={isFavourite ? heart : heartOutline} size="large" />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
|
||||
<IonCard style={{backgroundImage: `url('${product.image}')`}}>
|
||||
<IonCardHeader>
|
||||
<IonCardTitle>{product.title}</IonCardTitle>
|
||||
<IonCardSubtitle>{product.price}</IonCardSubtitle>
|
||||
</IonCardHeader>
|
||||
</IonCard>
|
||||
|
||||
<div className="ion-padding">
|
||||
|
||||
<IonRow className="ion-align-items-center">
|
||||
<IonCol>
|
||||
<IonText size="large" className="page-title">
|
||||
<IonNote>shop</IonNote>
|
||||
<IonLabel>{category ? category : "Favourite"}</IonLabel>
|
||||
</IonText>
|
||||
</IonCol>
|
||||
|
||||
<ProductReviews reviews={product.reviews} />
|
||||
</IonRow>
|
||||
<h2>Product Description</h2>
|
||||
<IonText>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam elit felis, molestie id venenatis at, commodo ac tortor. Pellentesque tempus aliquet purus, sed vulputate elit tempus ut.</IonText>
|
||||
|
||||
<h2>Product Specifications</h2>
|
||||
<ProductSpecificationsAccordion contentRef={contentRef} type={category} />
|
||||
</div>
|
||||
</IonContent>
|
||||
|
||||
<IonFooter collapse="fade">
|
||||
<IonToolbar>
|
||||
<IonRow className="ion-justify-content-between ion-align-items-center">
|
||||
<IonCol size="3">
|
||||
<IonButton expand="full" color="light">{product.price}</IonButton>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="8" className="ion-text-right">
|
||||
<AddToCartButton product={product} />
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonToolbar>
|
||||
</IonFooter>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
import { IonCol, IonIcon, IonNote } from "@ionic/react";
|
||||
import { star } from "ionicons/icons";
|
||||
import { useEffect, useState } from "react";
|
||||
import { randomCount } from "../utils";
|
||||
|
||||
export const ProductReviews = () => {
|
||||
|
||||
// This count could come from the product (if real data was fed)
|
||||
const [reviewCount, setReviewCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
setReviewCount(randomCount());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<IonCol className="ion-text-right">
|
||||
<IonIcon color="warning" icon={star} />
|
||||
|
||||
<IonNote>{reviewCount} review{reviewCount > 1 && "s"}</IonNote>
|
||||
</IonCol>
|
||||
);
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
import { IonAccordion, IonAccordionGroup, IonItem, IonLabel, IonList, IonNote } from "@ionic/react";
|
||||
import { useRef } from "react";
|
||||
import { productSpecs } from "../utils";
|
||||
|
||||
export const ProductSpecificationsAccordion = ({type, contentRef}) => {
|
||||
|
||||
const accordionGroupRef = useRef(null);
|
||||
|
||||
const log = () => {
|
||||
|
||||
const selectedAccordion = accordionGroupRef.current.value;
|
||||
|
||||
if (selectedAccordion) {
|
||||
|
||||
setTimeout(() => contentRef.current.scrollToBottom(400), 200);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<IonAccordionGroup ref={accordionGroupRef} onIonChange={log}>
|
||||
{Object.keys(productSpecs).map((spec, index) => {
|
||||
|
||||
const {header, options, wrapText = false, noteColor = false} = productSpecs[spec];
|
||||
|
||||
return (
|
||||
|
||||
<IonAccordion key={`accordion_${header}_${index}`}>
|
||||
<IonItem slot="header" className="ion-no-padding">
|
||||
<IonLabel>{header}</IonLabel>
|
||||
</IonItem>
|
||||
|
||||
|
||||
<IonList slot="content" className="ion-no-padding">
|
||||
{options.map((option, index2) => {
|
||||
|
||||
const {label, value} = option;
|
||||
|
||||
return (
|
||||
|
||||
<IonItem key={`accordion_${header}_${option}_${index2}`} className="ion-no-padding">
|
||||
<IonLabel>
|
||||
<h3>{label}</h3>
|
||||
</IonLabel>
|
||||
<IonLabel className={wrapText && "ion-text-wrap"}>
|
||||
<IonNote color={noteColor ? (value ? "success" : "danger") : "medium"}>
|
||||
{noteColor ? (value ? "In stock" : "Out of stock") : value}
|
||||
</IonNote>
|
||||
</IonLabel>
|
||||
</IonItem>
|
||||
);
|
||||
})}
|
||||
</IonList>
|
||||
</IonAccordion>
|
||||
);
|
||||
})}
|
||||
</IonAccordionGroup>
|
||||
);
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
import { IonButton, IonContent, IonHeader, IonLabel, IonNote, IonPage, IonRouterLink, IonRow, IonTitle, IonToolbar } from '@ionic/react';
|
||||
import { capitalize, productInfo } from '../utils';
|
||||
|
||||
import styles from "./Categories.module.scss";
|
||||
|
||||
const Categories = () => {
|
||||
|
||||
const categories = Object.keys(productInfo);
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Ionic Shop</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large" className="page-title">
|
||||
<IonLabel>ionic</IonLabel>
|
||||
<IonNote>shop</IonNote>
|
||||
</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonRow>
|
||||
{categories.map(category => (
|
||||
|
||||
<IonRouterLink routerLink={`/categories/${category.toLowerCase()}`}>
|
||||
<div className={styles.categoryContainer}>
|
||||
<img src={productInfo[category].coverImage} alt="cover" />
|
||||
<p>{capitalize(category)}</p>
|
||||
</div>
|
||||
</IonRouterLink>
|
||||
|
||||
// <IonButton key={c} routerLink={`/categories/${c.toLowerCase()}`}>{capitalize(c)}</IonButton>
|
||||
))}
|
||||
</IonRow>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Categories;
|
@@ -0,0 +1,20 @@
|
||||
.categoryContainer {
|
||||
|
||||
display: flex;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.categoryContainer p {
|
||||
|
||||
display: flex;
|
||||
justify-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
width: 50%;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
margin: 0 auto;
|
||||
font-size: 2rem;
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
import { IonButton, IonButtons, IonContent, IonHeader, IonIcon, IonLabel, IonNote, IonPage, IonRouterLink, IonRow, IonTitle, IonToolbar, useIonRouter } from '@ionic/react';
|
||||
import { chevronBack } from 'ionicons/icons';
|
||||
import { useParams } from 'react-router';
|
||||
import { capitalize, productInfo } from '../utils';
|
||||
|
||||
import styles from "./Categories.module.scss";
|
||||
|
||||
const Category = () => {
|
||||
|
||||
const router = useIonRouter();
|
||||
const { category } = useParams();
|
||||
const productTypes = Object.keys(productInfo[category].productTypes);
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
|
||||
<IonButtons slot="start">
|
||||
<IonButton className="custom-back" onClick={() => router.goBack()}>
|
||||
<IonIcon icon={chevronBack} />
|
||||
<IonLabel>Back</IonLabel>
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
<IonTitle>{category}</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large" className="page-title">
|
||||
<IonNote>shop</IonNote>
|
||||
<IonLabel>{category}</IonLabel>
|
||||
</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonRow>
|
||||
{productTypes.map(product => (
|
||||
|
||||
<IonRouterLink key={`${category}_${product}`} routerLink={`/categories/${category}/${product.toLowerCase().replaceAll(" ", "_")}`}>
|
||||
<div className={styles.categoryContainer}>
|
||||
<img src={productInfo[category].productTypes[product].coverImage} alt="cover" />
|
||||
<p>{capitalize(product)}</p>
|
||||
</div>
|
||||
</IonRouterLink>
|
||||
))}
|
||||
</IonRow>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Category;
|
@@ -0,0 +1,77 @@
|
||||
import { IonCol, IonContent, IonGrid, IonHeader, IonIcon, IonImg, IonLabel, IonPage, IonRow, IonText, IonTitle, IonToolbar, useIonModal } from '@ionic/react';
|
||||
import { heartOutline } from 'ionicons/icons';
|
||||
import { useStoreState } from 'pullstate';
|
||||
import { useState } from 'react';
|
||||
import { ProductModal } from '../components/ProductModal';
|
||||
import { FavouritesStore } from '../store';
|
||||
import { getFavourites } from '../store/Selectors';
|
||||
|
||||
const Favourites = () => {
|
||||
|
||||
const favourites = useStoreState(FavouritesStore, getFavourites);
|
||||
|
||||
const [selectedProduct, setSelectedProduct] = useState([]);
|
||||
const [presentProductModal, dismissProductModal] = useIonModal(ProductModal, {
|
||||
|
||||
dismiss: () => dismissProductModal(),
|
||||
product: selectedProduct
|
||||
});
|
||||
|
||||
const handleProductModal = product => {
|
||||
|
||||
setSelectedProduct(product);
|
||||
presentProductModal();
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Favourites</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Favourites</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonGrid className="animate__animated">
|
||||
<IonRow>
|
||||
{favourites.map((product, index) => {
|
||||
|
||||
if (product.image !== null && product.image !== "" && !product.image.includes("Placeholder")) {
|
||||
return (
|
||||
<IonCol onClick={() => handleProductModal(product)} key={index} size="6" sizeXs="6" sizeSm="3" sizeMd="3" sizeXl="2">
|
||||
<IonImg src={product.image} style={{marginBottom: "0.25rem"}} />
|
||||
<IonLabel>
|
||||
<h3>{product.title}</h3>
|
||||
<p>{product.price}</p>
|
||||
</IonLabel>
|
||||
</IonCol>
|
||||
);
|
||||
} else return null;
|
||||
})}
|
||||
</IonRow>
|
||||
|
||||
{favourites.length === 0 &&
|
||||
<IonRow className="ion-text-center ion-justify-content-center">
|
||||
<IonCol size="10">
|
||||
<IonText color="dark">
|
||||
<h1>No favourites yet</h1>
|
||||
</IonText>
|
||||
|
||||
<IonText color="medium">
|
||||
<h3>Add some by clicking the <IonIcon icon={heartOutline} color="danger" /> icon on a product</h3>
|
||||
</IonText>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
}
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Favourites;
|
161
03_source/mobile/src/pages/DemoReactShopUi/pages/ProductType.jsx
Normal file
161
03_source/mobile/src/pages/DemoReactShopUi/pages/ProductType.jsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { IonBreadcrumb, IonBreadcrumbs, IonButton, IonButtons, IonCol, IonContent, IonGrid, IonHeader, IonIcon, IonImg, IonLabel, IonNote, IonPage, IonRow, IonSearchbar, IonTitle, IonToolbar, useIonModal, useIonRouter } from '@ionic/react';
|
||||
import { chevronBack, filter } from 'ionicons/icons';
|
||||
import { useRef } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { FilterModal } from '../components/FilterModal';
|
||||
import { ProductModal } from '../components/ProductModal';
|
||||
import { capitalize, productInfo } from '../utils';
|
||||
|
||||
const ProductType = () => {
|
||||
|
||||
const router = useIonRouter();
|
||||
const { category, type } = useParams();
|
||||
const productsRef = useRef();
|
||||
|
||||
const [products, setProducts] = useState([]);
|
||||
const [filteredProducts, setFilteredProducts] = useState([]);
|
||||
const [filterCriteria, setFilterCriteria] = useState("None");
|
||||
|
||||
const filters = productInfo[category].productTypes[type].filters;
|
||||
const searchPlaceholder = productInfo[category].productTypes[type].searchPlaceholder;
|
||||
|
||||
const [selectedProduct, setSelectedProduct] = useState([]);
|
||||
const [presentProductModal, dismissProductModal] = useIonModal(ProductModal, {
|
||||
|
||||
dismiss: () => dismissProductModal(),
|
||||
category,
|
||||
type,
|
||||
product: selectedProduct
|
||||
});
|
||||
|
||||
const handleProductModal = product => {
|
||||
|
||||
setSelectedProduct(product);
|
||||
presentProductModal();
|
||||
}
|
||||
|
||||
const [present, dismiss] = useIonModal(FilterModal, {
|
||||
|
||||
dismiss: () => dismiss(),
|
||||
|
||||
filterCriteria,
|
||||
setFilterCriteria,
|
||||
productsRef,
|
||||
filters
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const getProducts = async () => {
|
||||
|
||||
const response = await fetch(`/data/${category}/${type}.json`);
|
||||
const data = await response.json();
|
||||
setProducts(data);
|
||||
setFilteredProducts(data);
|
||||
}
|
||||
|
||||
getProducts();
|
||||
}, [category, type]);
|
||||
|
||||
const openModal = () => {
|
||||
|
||||
present({
|
||||
breakpoints: [0, 0.25],
|
||||
initialBreakpoint: 0.25,
|
||||
backdropBreakpoint: 0
|
||||
});
|
||||
}
|
||||
|
||||
const performSearch = e => {
|
||||
|
||||
const searchCriteria = e.target.value.toLowerCase();
|
||||
let tempFilteredProducts = [...products];
|
||||
|
||||
if (searchCriteria !== "") {
|
||||
|
||||
tempFilteredProducts = tempFilteredProducts.filter(product => product.title.toLowerCase().includes(searchCriteria));
|
||||
setFilteredProducts(tempFilteredProducts);
|
||||
} else {
|
||||
|
||||
setFilteredProducts(products);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
|
||||
<IonButtons slot="start">
|
||||
<IonButton className="custom-back" onClick={() => router.goBack()}>
|
||||
<IonIcon icon={chevronBack} />
|
||||
<IonLabel>Back</IonLabel>
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
<IonTitle>{capitalize(type)}</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large" className="page-title">
|
||||
<IonNote>shop</IonNote>
|
||||
<IonLabel>{category}</IonLabel>
|
||||
</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonRow className="ion-align-items-center ion-text-center ion-justify-content-between">
|
||||
|
||||
<IonCol size="10">
|
||||
<IonBreadcrumbs>
|
||||
<IonBreadcrumb active={false} color="medium">
|
||||
{capitalize(category)}
|
||||
</IonBreadcrumb>
|
||||
<IonBreadcrumb separator={false} color={filterCriteria !== "None" && "medium"} active={filterCriteria === "None" ? true : false}>
|
||||
{capitalize(type)}
|
||||
</IonBreadcrumb>
|
||||
{filterCriteria !== "None" &&
|
||||
<IonBreadcrumb color="dark" active={true}>
|
||||
<IonIcon slot="start" icon={filter} />
|
||||
{filterCriteria}
|
||||
</IonBreadcrumb>
|
||||
}
|
||||
</IonBreadcrumbs>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="2" className="ion-text-right">
|
||||
<div onClick={openModal} style={{ display: 'flex', color: "#828282", float:"right", padding: "0.5rem", backgroundColor: "#F4F5F8", marginRight: "0.5rem", width: "fit-content"}}>
|
||||
<IonIcon icon={filter} />
|
||||
<IonLabel> Filter</IonLabel>
|
||||
</div>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonSearchbar color="light" animated={true} style={{"--border-radius": "none"}} placeholder={`Try '${searchPlaceholder}'`} onIonChange={e => performSearch(e)} />
|
||||
|
||||
<IonGrid ref={productsRef} className="animate__animated">
|
||||
<IonRow>
|
||||
{filteredProducts.map((product, index) => {
|
||||
|
||||
if (product.image !== null && product.image !== "" && !product.image.includes("Placeholder")) {
|
||||
return (
|
||||
<IonCol onClick={() => handleProductModal(product)} key={index} size="6" sizeXs="6" sizeSm="3" sizeMd="3" sizeXl="2" style={{display: ((filterCriteria !== "None" && product.title.toLowerCase().includes(filterCriteria.toLowerCase())) || filterCriteria === "None") ? "block" : "none"}}>
|
||||
<IonImg src={product.image} style={{marginBottom: "0.25rem"}} />
|
||||
<IonLabel>
|
||||
<h3>{product.title}</h3>
|
||||
<p>{product.price}</p>
|
||||
</IonLabel>
|
||||
</IonCol>
|
||||
);
|
||||
} else return null;
|
||||
})}
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductType;
|
37
03_source/mobile/src/pages/DemoReactShopUi/pages/index.js
Normal file
37
03_source/mobile/src/pages/DemoReactShopUi/pages/index.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { heartOutline, homeOutline, shirtOutline } from "ionicons/icons";
|
||||
|
||||
import Categories from "./Categories";
|
||||
import Favourites from "./Favourites";
|
||||
import ProductType from "./ProductType";
|
||||
import Category from "./Category";
|
||||
|
||||
export const pages = [
|
||||
|
||||
{
|
||||
href: "/categories",
|
||||
icon: shirtOutline,
|
||||
component: Categories,
|
||||
default: true,
|
||||
isTab: true
|
||||
},
|
||||
{
|
||||
href: "/categories/:category/:type",
|
||||
component: ProductType,
|
||||
default: false,
|
||||
isTab: false
|
||||
},
|
||||
{
|
||||
href: "/categories/:category",
|
||||
icon: shirtOutline,
|
||||
component: Category,
|
||||
default: true,
|
||||
isTab: false
|
||||
},
|
||||
{
|
||||
href: "/favourites",
|
||||
icon: heartOutline,
|
||||
component: Favourites,
|
||||
default: false,
|
||||
isTab: true
|
||||
}
|
||||
];
|
@@ -0,0 +1,27 @@
|
||||
import { Store } from "pullstate";
|
||||
|
||||
const CartStore = new Store({
|
||||
|
||||
cart: []
|
||||
});
|
||||
|
||||
export default CartStore;
|
||||
|
||||
export const addToCart = product => {
|
||||
|
||||
const currentCart = CartStore.getRawState().cart;
|
||||
const added = !currentCart.includes(product);
|
||||
|
||||
CartStore.update(s => {
|
||||
|
||||
if (currentCart.includes(product)) {
|
||||
|
||||
s.cart = currentCart.filter(current => current !== product);
|
||||
} else {
|
||||
|
||||
s.cart = [ ...s.cart, product ];
|
||||
}
|
||||
});
|
||||
|
||||
return added;
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
import { Store } from "pullstate";
|
||||
|
||||
const FavouritesStore = new Store({
|
||||
|
||||
favourites: []
|
||||
});
|
||||
|
||||
export default FavouritesStore;
|
||||
|
||||
export const checkIfFavourite = product => {
|
||||
|
||||
const currentFavourites = FavouritesStore.getRawState().favourites;
|
||||
const isFavourite = currentFavourites.includes(product);
|
||||
|
||||
return isFavourite;
|
||||
}
|
||||
|
||||
export const addToFavourites = (product, category) => {
|
||||
|
||||
const currentFavourites = FavouritesStore.getRawState().favourites;
|
||||
const added = !currentFavourites.includes(product);
|
||||
|
||||
FavouritesStore.update(s => {
|
||||
|
||||
if (!added) {
|
||||
|
||||
s.favourites = currentFavourites.filter(current => current !== product);
|
||||
} else {
|
||||
|
||||
s.favourites = [ ...s.favourites, product ];
|
||||
}
|
||||
});
|
||||
|
||||
return added;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user